@sentrial/sdk 0.3.3 → 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.js CHANGED
@@ -68,7 +68,22 @@ var ValidationError = class extends SentrialError {
68
68
  };
69
69
 
70
70
  // src/redact.ts
71
- import { createHash } from "crypto";
71
+ var _createHash;
72
+ try {
73
+ const mod = eval("require")("crypto");
74
+ if (mod?.createHash) {
75
+ _createHash = mod.createHash;
76
+ }
77
+ } catch {
78
+ }
79
+ function getCreateHash() {
80
+ if (!_createHash) {
81
+ throw new Error(
82
+ 'Sentrial PII hash mode requires Node.js crypto module. Use mode "label" or "remove" in browser/edge environments.'
83
+ );
84
+ }
85
+ return _createHash;
86
+ }
72
87
  var DEFAULT_FIELDS = [
73
88
  "userInput",
74
89
  "assistantOutput",
@@ -95,7 +110,7 @@ var BUILTIN_PATTERNS = {
95
110
  ipAddresses: { pattern: IP_ADDRESS_PATTERN, label: "IP_ADDRESS" }
96
111
  };
97
112
  function hashValue(value) {
98
- return createHash("sha256").update(value).digest("hex").slice(0, 6);
113
+ return getCreateHash()("sha256").update(value).digest("hex").slice(0, 6);
99
114
  }
100
115
  function replaceMatch(match, label, mode) {
101
116
  switch (mode) {
@@ -157,6 +172,59 @@ function redactPayload(payload, config) {
157
172
  return result;
158
173
  }
159
174
 
175
+ // src/async-context.ts
176
+ var _AsyncLocalStorage = null;
177
+ try {
178
+ const mod = eval("require")("node:async_hooks");
179
+ if (mod?.AsyncLocalStorage) {
180
+ _AsyncLocalStorage = mod.AsyncLocalStorage;
181
+ }
182
+ } catch {
183
+ }
184
+ var NodeContextVar = class {
185
+ _storage;
186
+ _defaultValue;
187
+ constructor(defaultValue) {
188
+ this._storage = new _AsyncLocalStorage();
189
+ this._defaultValue = defaultValue;
190
+ }
191
+ get() {
192
+ const store = this._storage.getStore();
193
+ return store !== void 0 ? store : this._defaultValue;
194
+ }
195
+ set(value) {
196
+ const previous = this.get();
197
+ this._storage.enterWith(value);
198
+ return { _previous: previous };
199
+ }
200
+ reset(token) {
201
+ this._storage.enterWith(token._previous);
202
+ }
203
+ };
204
+ var SimpleContextVar = class {
205
+ _value;
206
+ constructor(defaultValue) {
207
+ this._value = defaultValue;
208
+ }
209
+ get() {
210
+ return this._value;
211
+ }
212
+ set(value) {
213
+ const previous = this._value;
214
+ this._value = value;
215
+ return { _previous: previous };
216
+ }
217
+ reset(token) {
218
+ this._value = token._previous;
219
+ }
220
+ };
221
+ function createContextVar(defaultValue) {
222
+ if (_AsyncLocalStorage) {
223
+ return new NodeContextVar(defaultValue);
224
+ }
225
+ return new SimpleContextVar(defaultValue);
226
+ }
227
+
160
228
  // src/cost.ts
161
229
  var OPENAI_PRICING = {
162
230
  "gpt-5.2": { input: 5, output: 15 },
@@ -208,7 +276,8 @@ var GOOGLE_PRICING = {
208
276
  "gemini-1.0-pro": { input: 0.5, output: 1.5 }
209
277
  };
210
278
  function findModelKey(model, pricing) {
211
- for (const key of Object.keys(pricing)) {
279
+ const keys = Object.keys(pricing).sort((a, b) => b.length - a.length);
280
+ for (const key of keys) {
212
281
  if (model.startsWith(key)) {
213
282
  return key;
214
283
  }
@@ -236,179 +305,783 @@ function calculateGoogleCost(params) {
236
305
  return calculateCost(inputTokens, outputTokens, GOOGLE_PRICING[modelKey]);
237
306
  }
238
307
 
239
- // src/types.ts
240
- var EventType = /* @__PURE__ */ ((EventType2) => {
241
- EventType2["TOOL_CALL"] = "tool_call";
242
- EventType2["LLM_DECISION"] = "llm_decision";
243
- EventType2["STATE_CHANGE"] = "state_change";
244
- EventType2["ERROR"] = "error";
245
- return EventType2;
246
- })(EventType || {});
247
-
248
- // src/client.ts
249
- var DEFAULT_API_URL = "https://api.sentrial.com";
250
- var MAX_RETRIES = 3;
251
- var INITIAL_BACKOFF_MS = 500;
252
- var MAX_BACKOFF_MS = 8e3;
253
- var BACKOFF_MULTIPLIER = 2;
254
- var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
255
- var REQUEST_TIMEOUT_MS = 1e4;
256
- var SentrialClient = class {
257
- apiUrl;
258
- apiKey;
259
- failSilently;
260
- piiConfig;
261
- piiConfigNeedsHydration = false;
262
- piiHydrationPromise;
263
- currentState = {};
264
- constructor(config = {}) {
265
- this.apiUrl = (config.apiUrl ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_URL : void 0) ?? DEFAULT_API_URL).replace(/\/$/, "");
266
- this.apiKey = config.apiKey ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_KEY : void 0);
267
- this.failSilently = config.failSilently ?? true;
268
- if (config.pii === true) {
269
- this.piiConfig = { enabled: true };
270
- this.piiConfigNeedsHydration = true;
271
- } else if (config.pii && typeof config.pii === "object") {
272
- this.piiConfig = config.pii;
273
- this.piiConfigNeedsHydration = false;
274
- }
308
+ // src/wrappers.ts
309
+ var _currentSessionId = createContextVar(null);
310
+ var _currentClient = createContextVar(null);
311
+ var _defaultClient = null;
312
+ function setSessionContext(sessionId, client) {
313
+ _currentSessionId.set(sessionId);
314
+ if (client) {
315
+ _currentClient.set(client);
275
316
  }
276
- /**
277
- * Fetch the organization's PII config from the server.
278
- *
279
- * Called lazily on the first request when `pii: true` was passed to the constructor.
280
- * Uses a single shared promise so concurrent requests don't trigger duplicate fetches.
281
- */
282
- async hydratePiiConfig() {
283
- if (!this.piiConfigNeedsHydration) return;
284
- if (this.piiHydrationPromise) {
285
- await this.piiHydrationPromise;
286
- return;
287
- }
288
- this.piiHydrationPromise = (async () => {
289
- try {
290
- const headers = {};
291
- if (this.apiKey) {
292
- headers["Authorization"] = `Bearer ${this.apiKey}`;
293
- }
294
- const controller = new AbortController();
295
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
296
- let response;
297
- try {
298
- response = await fetch(`${this.apiUrl}/api/sdk/pii-config`, {
299
- method: "GET",
300
- headers,
301
- signal: controller.signal
302
- });
303
- } finally {
304
- clearTimeout(timeoutId);
305
- }
306
- if (response.ok) {
307
- const data = await response.json();
308
- if (data.config) {
309
- this.piiConfig = {
310
- enabled: data.config.enabled,
311
- mode: data.config.mode,
312
- fields: data.config.fields,
313
- builtinPatterns: data.config.builtinPatterns,
314
- customPatterns: (data.config.customPatterns || []).map(
315
- (cp) => ({
316
- pattern: new RegExp(cp.pattern, "g"),
317
- label: cp.label
318
- })
319
- ),
320
- enhancedDetection: data.config.enhancedDetection
321
- };
322
- }
323
- }
324
- } catch {
325
- }
326
- this.piiConfigNeedsHydration = false;
327
- })();
328
- await this.piiHydrationPromise;
317
+ }
318
+ function clearSessionContext() {
319
+ _currentSessionId.set(null);
320
+ _currentClient.set(null);
321
+ }
322
+ function getSessionContext() {
323
+ return _currentSessionId.get();
324
+ }
325
+ function setDefaultClient(client) {
326
+ _defaultClient = client;
327
+ }
328
+ function _setSessionContextWithTokens(sessionId, client) {
329
+ const _sessionToken = _currentSessionId.set(sessionId);
330
+ const _clientToken = client ? _currentClient.set(client) : _currentClient.set(_currentClient.get());
331
+ return { _sessionToken, _clientToken };
332
+ }
333
+ function _restoreSessionContext(tokens) {
334
+ _currentSessionId.reset(tokens._sessionToken);
335
+ _currentClient.reset(tokens._clientToken);
336
+ }
337
+ function getTrackingClient() {
338
+ return _currentClient.get() ?? _defaultClient;
339
+ }
340
+ function wrapOpenAI(client, options = {}) {
341
+ const { trackWithoutSession = false } = options;
342
+ const chat = client.chat;
343
+ if (!chat?.completions?.create) {
344
+ console.warn("Sentrial: OpenAI client does not have chat.completions.create");
345
+ return client;
329
346
  }
330
- /**
331
- * Make an HTTP request with retry logic and exponential backoff.
332
- *
333
- * Retries on transient failures (network errors, timeouts, 429/5xx).
334
- * Up to MAX_RETRIES attempts with exponential backoff.
335
- */
336
- async safeRequest(method, url, body) {
337
- if (this.piiConfigNeedsHydration) {
338
- await this.hydratePiiConfig();
347
+ const originalCreate = chat.completions.create.bind(chat.completions);
348
+ chat.completions.create = async function(...args) {
349
+ const startTime = Date.now();
350
+ const params = args[0] ?? {};
351
+ const messages = params.messages ?? [];
352
+ const model = params.model ?? "unknown";
353
+ const isStreaming = params.stream === true;
354
+ if (isStreaming && !params.stream_options?.include_usage) {
355
+ args[0] = { ...params, stream_options: { ...params.stream_options, include_usage: true } };
339
356
  }
340
- let lastError;
341
- let backoff = INITIAL_BACKOFF_MS;
342
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
343
- try {
344
- const headers = {
345
- "Content-Type": "application/json"
346
- };
347
- if (this.apiKey) {
348
- headers["Authorization"] = `Bearer ${this.apiKey}`;
349
- }
350
- const finalBody = this.piiConfig && body && typeof body === "object" ? redactPayload(body, this.piiConfig) : body;
351
- const controller = new AbortController();
352
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
353
- let response;
354
- try {
355
- response = await fetch(url, {
356
- method,
357
- headers,
358
- body: finalBody ? JSON.stringify(finalBody) : void 0,
359
- signal: controller.signal
360
- });
361
- } finally {
362
- clearTimeout(timeoutId);
363
- }
364
- if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
365
- await this.sleep(backoff);
366
- backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
367
- continue;
368
- }
369
- if (!response.ok) {
370
- const errorBody = await response.text();
371
- let errorData = {};
372
- try {
373
- errorData = JSON.parse(errorBody);
374
- } catch {
375
- }
376
- const error = new ApiError(
377
- errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
378
- response.status,
379
- errorData.error?.code
380
- );
381
- if (this.failSilently) {
382
- console.warn(`Sentrial: Request failed (${method} ${url}):`, error.message);
383
- return null;
384
- }
385
- throw error;
386
- }
387
- return await response.json();
388
- } catch (error) {
389
- if (error instanceof ApiError) {
390
- throw error;
391
- }
392
- lastError = error instanceof Error ? error : new Error(String(error));
393
- if (attempt < MAX_RETRIES) {
394
- await this.sleep(backoff);
395
- backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
396
- continue;
397
- }
357
+ try {
358
+ const response = await originalCreate(...args);
359
+ if (isStreaming) {
360
+ return wrapOpenAIStream(response, { startTime, messages, model, trackWithoutSession });
398
361
  }
362
+ const durationMs = Date.now() - startTime;
363
+ const promptTokens = response.usage?.prompt_tokens ?? 0;
364
+ const completionTokens = response.usage?.completion_tokens ?? 0;
365
+ const totalTokens = response.usage?.total_tokens ?? 0;
366
+ let outputContent = "";
367
+ if (response.choices?.[0]?.message?.content) {
368
+ outputContent = response.choices[0].message.content;
369
+ }
370
+ const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
371
+ trackLLMCall({
372
+ provider: "openai",
373
+ model,
374
+ messages,
375
+ output: outputContent,
376
+ promptTokens,
377
+ completionTokens,
378
+ totalTokens,
379
+ cost,
380
+ durationMs,
381
+ trackWithoutSession
382
+ });
383
+ return response;
384
+ } catch (error) {
385
+ const durationMs = Date.now() - startTime;
386
+ trackLLMError({
387
+ provider: "openai",
388
+ model,
389
+ messages,
390
+ error,
391
+ durationMs,
392
+ trackWithoutSession
393
+ });
394
+ throw error;
399
395
  }
400
- const networkError = new NetworkError(
401
- lastError?.message ?? "Unknown network error",
402
- lastError
403
- );
404
- if (this.failSilently) {
405
- console.warn(`Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`, networkError.message);
406
- return null;
407
- }
408
- throw networkError;
396
+ };
397
+ return client;
398
+ }
399
+ function wrapAnthropic(client, options = {}) {
400
+ const { trackWithoutSession = false } = options;
401
+ const messages = client.messages;
402
+ if (!messages?.create) {
403
+ console.warn("Sentrial: Anthropic client does not have messages.create");
404
+ return client;
409
405
  }
410
- sleep(ms) {
411
- return new Promise((resolve) => setTimeout(resolve, ms));
406
+ const originalCreate = messages.create.bind(messages);
407
+ messages.create = async function(...args) {
408
+ const startTime = Date.now();
409
+ const params = args[0] ?? {};
410
+ const inputMessages = params.messages ?? [];
411
+ const model = params.model ?? "unknown";
412
+ const system = params.system ?? "";
413
+ const isStreaming = params.stream === true;
414
+ try {
415
+ const response = await originalCreate(...args);
416
+ if (isStreaming) {
417
+ return wrapAnthropicStream(response, {
418
+ startTime,
419
+ messages: inputMessages,
420
+ model,
421
+ system,
422
+ trackWithoutSession
423
+ });
424
+ }
425
+ const durationMs = Date.now() - startTime;
426
+ const promptTokens = response.usage?.input_tokens ?? 0;
427
+ const completionTokens = response.usage?.output_tokens ?? 0;
428
+ const totalTokens = promptTokens + completionTokens;
429
+ let outputContent = "";
430
+ if (response.content) {
431
+ for (const block of response.content) {
432
+ if (block.type === "text") {
433
+ outputContent += block.text;
434
+ }
435
+ }
436
+ }
437
+ const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
438
+ const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
439
+ trackLLMCall({
440
+ provider: "anthropic",
441
+ model,
442
+ messages: fullMessages,
443
+ output: outputContent,
444
+ promptTokens,
445
+ completionTokens,
446
+ totalTokens,
447
+ cost,
448
+ durationMs,
449
+ trackWithoutSession
450
+ });
451
+ return response;
452
+ } catch (error) {
453
+ const durationMs = Date.now() - startTime;
454
+ trackLLMError({
455
+ provider: "anthropic",
456
+ model,
457
+ messages: inputMessages,
458
+ error,
459
+ durationMs,
460
+ trackWithoutSession
461
+ });
462
+ throw error;
463
+ }
464
+ };
465
+ return client;
466
+ }
467
+ function wrapGoogle(model, options = {}) {
468
+ const { trackWithoutSession = false } = options;
469
+ const originalGenerate = model.generateContent;
470
+ if (!originalGenerate) {
471
+ console.warn("Sentrial: Google model does not have generateContent");
472
+ return model;
473
+ }
474
+ model.generateContent = async function(...args) {
475
+ const startTime = Date.now();
476
+ const contents = args[0];
477
+ const modelName = model.model ?? "gemini-unknown";
478
+ const messages = googleContentsToMessages(contents);
479
+ try {
480
+ const response = await originalGenerate.apply(model, args);
481
+ const durationMs = Date.now() - startTime;
482
+ let promptTokens = 0;
483
+ let completionTokens = 0;
484
+ const usageMeta = response.response?.usageMetadata ?? response.usageMetadata;
485
+ if (usageMeta) {
486
+ promptTokens = usageMeta.promptTokenCount ?? 0;
487
+ completionTokens = usageMeta.candidatesTokenCount ?? 0;
488
+ }
489
+ const totalTokens = promptTokens + completionTokens;
490
+ let outputContent = "";
491
+ try {
492
+ outputContent = response.response?.text?.() ?? response.text?.() ?? "";
493
+ } catch {
494
+ }
495
+ const cost = calculateGoogleCost({ model: modelName, inputTokens: promptTokens, outputTokens: completionTokens });
496
+ trackLLMCall({
497
+ provider: "google",
498
+ model: modelName,
499
+ messages,
500
+ output: outputContent,
501
+ promptTokens,
502
+ completionTokens,
503
+ totalTokens,
504
+ cost,
505
+ durationMs,
506
+ trackWithoutSession
507
+ });
508
+ return response;
509
+ } catch (error) {
510
+ const durationMs = Date.now() - startTime;
511
+ trackLLMError({
512
+ provider: "google",
513
+ model: modelName,
514
+ messages,
515
+ error,
516
+ durationMs,
517
+ trackWithoutSession
518
+ });
519
+ throw error;
520
+ }
521
+ };
522
+ return model;
523
+ }
524
+ function googleContentsToMessages(contents) {
525
+ if (typeof contents === "string") {
526
+ return [{ role: "user", content: contents }];
527
+ }
528
+ if (Array.isArray(contents)) {
529
+ return contents.map((item) => {
530
+ if (typeof item === "string") {
531
+ return { role: "user", content: item };
532
+ }
533
+ if (item && typeof item === "object") {
534
+ return { role: item.role ?? "user", content: String(item.content ?? item) };
535
+ }
536
+ return { role: "user", content: String(item) };
537
+ });
538
+ }
539
+ return [{ role: "user", content: String(contents) }];
540
+ }
541
+ function wrapLLM(client, provider) {
542
+ if (provider === "openai" || client.chat?.completions?.create) {
543
+ return wrapOpenAI(client);
544
+ }
545
+ if (provider === "anthropic" || client.messages?.create) {
546
+ return wrapAnthropic(client);
547
+ }
548
+ if (provider === "google" || client.generateContent) {
549
+ return wrapGoogle(client);
550
+ }
551
+ console.warn("Sentrial: Unknown LLM client type. No auto-tracking applied.");
552
+ return client;
553
+ }
554
+ function wrapOpenAIStream(stream, ctx) {
555
+ let fullContent = "";
556
+ let usage = null;
557
+ let tracked = false;
558
+ const originalIterator = stream[Symbol.asyncIterator]?.bind(stream);
559
+ if (!originalIterator) return stream;
560
+ const trackResult = () => {
561
+ if (tracked) return;
562
+ tracked = true;
563
+ const durationMs = Date.now() - ctx.startTime;
564
+ const promptTokens = usage?.prompt_tokens ?? 0;
565
+ const completionTokens = usage?.completion_tokens ?? 0;
566
+ const totalTokens = usage?.total_tokens ?? promptTokens + completionTokens;
567
+ const cost = calculateOpenAICost({ model: ctx.model, inputTokens: promptTokens, outputTokens: completionTokens });
568
+ trackLLMCall({
569
+ provider: "openai",
570
+ model: ctx.model,
571
+ messages: ctx.messages,
572
+ output: fullContent,
573
+ promptTokens,
574
+ completionTokens,
575
+ totalTokens,
576
+ cost,
577
+ durationMs,
578
+ trackWithoutSession: ctx.trackWithoutSession
579
+ });
580
+ };
581
+ return new Proxy(stream, {
582
+ get(target, prop, receiver) {
583
+ if (prop === Symbol.asyncIterator) {
584
+ return function() {
585
+ const iter = originalIterator();
586
+ return {
587
+ async next() {
588
+ const result = await iter.next();
589
+ if (!result.done) {
590
+ const chunk = result.value;
591
+ const delta = chunk.choices?.[0]?.delta?.content;
592
+ if (delta) fullContent += delta;
593
+ if (chunk.usage) usage = chunk.usage;
594
+ } else {
595
+ trackResult();
596
+ }
597
+ return result;
598
+ },
599
+ async return(value) {
600
+ trackResult();
601
+ return iter.return?.(value) ?? { done: true, value: void 0 };
602
+ },
603
+ async throw(error) {
604
+ return iter.throw?.(error) ?? { done: true, value: void 0 };
605
+ }
606
+ };
607
+ };
608
+ }
609
+ return Reflect.get(target, prop, receiver);
610
+ }
611
+ });
612
+ }
613
+ function wrapAnthropicStream(stream, ctx) {
614
+ let fullContent = "";
615
+ let inputTokens = 0;
616
+ let outputTokens = 0;
617
+ let tracked = false;
618
+ const originalIterator = stream[Symbol.asyncIterator]?.bind(stream);
619
+ if (!originalIterator) return stream;
620
+ const trackResult = () => {
621
+ if (tracked) return;
622
+ tracked = true;
623
+ const durationMs = Date.now() - ctx.startTime;
624
+ const totalTokens = inputTokens + outputTokens;
625
+ const cost = calculateAnthropicCost({ model: ctx.model, inputTokens, outputTokens });
626
+ const fullMessages = ctx.system ? [{ role: "system", content: ctx.system }, ...ctx.messages] : ctx.messages;
627
+ trackLLMCall({
628
+ provider: "anthropic",
629
+ model: ctx.model,
630
+ messages: fullMessages,
631
+ output: fullContent,
632
+ promptTokens: inputTokens,
633
+ completionTokens: outputTokens,
634
+ totalTokens,
635
+ cost,
636
+ durationMs,
637
+ trackWithoutSession: ctx.trackWithoutSession
638
+ });
639
+ };
640
+ return new Proxy(stream, {
641
+ get(target, prop, receiver) {
642
+ if (prop === Symbol.asyncIterator) {
643
+ return function() {
644
+ const iter = originalIterator();
645
+ return {
646
+ async next() {
647
+ const result = await iter.next();
648
+ if (!result.done) {
649
+ const event = result.value;
650
+ if (event.type === "content_block_delta" && event.delta?.text) {
651
+ fullContent += event.delta.text;
652
+ }
653
+ if (event.type === "message_start" && event.message?.usage) {
654
+ inputTokens = event.message.usage.input_tokens ?? 0;
655
+ }
656
+ if (event.type === "message_delta" && event.usage) {
657
+ outputTokens = event.usage.output_tokens ?? 0;
658
+ }
659
+ } else {
660
+ trackResult();
661
+ }
662
+ return result;
663
+ },
664
+ async return(value) {
665
+ trackResult();
666
+ return iter.return?.(value) ?? { done: true, value: void 0 };
667
+ },
668
+ async throw(error) {
669
+ return iter.throw?.(error) ?? { done: true, value: void 0 };
670
+ }
671
+ };
672
+ };
673
+ }
674
+ return Reflect.get(target, prop, receiver);
675
+ }
676
+ });
677
+ }
678
+ function trackLLMCall(params) {
679
+ const client = getTrackingClient();
680
+ if (!client) return;
681
+ const sessionId = _currentSessionId.get();
682
+ if (!sessionId && !params.trackWithoutSession) {
683
+ return;
684
+ }
685
+ if (sessionId) {
686
+ client.trackToolCall({
687
+ sessionId,
688
+ toolName: `llm:${params.provider}:${params.model}`,
689
+ toolInput: {
690
+ messages: params.messages,
691
+ model: params.model,
692
+ provider: params.provider
693
+ },
694
+ toolOutput: {
695
+ content: params.output,
696
+ tokens: {
697
+ prompt: params.promptTokens,
698
+ completion: params.completionTokens,
699
+ total: params.totalTokens
700
+ },
701
+ cost_usd: params.cost
702
+ },
703
+ reasoning: `LLM call to ${params.provider} ${params.model}`,
704
+ estimatedCost: params.cost,
705
+ tokenCount: params.totalTokens,
706
+ metadata: {
707
+ provider: params.provider,
708
+ model: params.model,
709
+ duration_ms: params.durationMs,
710
+ prompt_tokens: params.promptTokens,
711
+ completion_tokens: params.completionTokens
712
+ }
713
+ }).catch((err) => {
714
+ console.warn("Sentrial: Failed to track LLM call:", err.message);
715
+ });
716
+ } else if (params.trackWithoutSession) {
717
+ client.createSession({
718
+ name: `LLM: ${params.provider}/${params.model}`,
719
+ agentName: `${params.provider}-wrapper`,
720
+ userId: "anonymous"
721
+ }).then((sid) => {
722
+ if (!sid) return;
723
+ return client.trackToolCall({
724
+ sessionId: sid,
725
+ toolName: `llm:${params.provider}:${params.model}`,
726
+ toolInput: {
727
+ messages: params.messages,
728
+ model: params.model,
729
+ provider: params.provider
730
+ },
731
+ toolOutput: {
732
+ content: params.output,
733
+ tokens: {
734
+ prompt: params.promptTokens,
735
+ completion: params.completionTokens,
736
+ total: params.totalTokens
737
+ },
738
+ cost_usd: params.cost
739
+ },
740
+ estimatedCost: params.cost,
741
+ tokenCount: params.totalTokens,
742
+ metadata: {
743
+ provider: params.provider,
744
+ model: params.model,
745
+ duration_ms: params.durationMs
746
+ }
747
+ }).then(() => {
748
+ return client.completeSession({
749
+ sessionId: sid,
750
+ success: true,
751
+ estimatedCost: params.cost,
752
+ promptTokens: params.promptTokens,
753
+ completionTokens: params.completionTokens,
754
+ totalTokens: params.totalTokens,
755
+ durationMs: params.durationMs
756
+ });
757
+ });
758
+ }).catch((err) => {
759
+ console.warn("Sentrial: Failed to track standalone LLM call:", err.message);
760
+ });
761
+ }
762
+ }
763
+ function trackLLMError(params) {
764
+ const client = getTrackingClient();
765
+ if (!client) return;
766
+ const sessionId = _currentSessionId.get();
767
+ if (!sessionId && !params.trackWithoutSession) {
768
+ return;
769
+ }
770
+ if (!sessionId) return;
771
+ client.trackError({
772
+ sessionId,
773
+ errorMessage: params.error.message,
774
+ errorType: params.error.name,
775
+ toolName: `llm:${params.provider}:${params.model}`,
776
+ metadata: {
777
+ provider: params.provider,
778
+ model: params.model,
779
+ duration_ms: params.durationMs
780
+ }
781
+ }).catch((err) => {
782
+ console.warn("Sentrial: Failed to track LLM error:", err.message);
783
+ });
784
+ }
785
+
786
+ // src/batcher.ts
787
+ var EventBatcher = class {
788
+ queue = [];
789
+ flushIntervalMs;
790
+ flushThreshold;
791
+ maxQueueSize;
792
+ timer = null;
793
+ sendFn;
794
+ flushing = false;
795
+ shutdownCalled = false;
796
+ exitHandler;
797
+ constructor(sendFn, config = {}) {
798
+ this.sendFn = sendFn;
799
+ this.flushIntervalMs = config.flushIntervalMs ?? 1e3;
800
+ this.flushThreshold = config.flushThreshold ?? 10;
801
+ this.maxQueueSize = config.maxQueueSize ?? 1e3;
802
+ this.timer = setInterval(() => {
803
+ void this.flush();
804
+ }, this.flushIntervalMs);
805
+ if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
806
+ this.timer.unref();
807
+ }
808
+ this.exitHandler = () => {
809
+ void this.shutdown();
810
+ };
811
+ if (typeof process !== "undefined" && process.on) {
812
+ process.on("beforeExit", this.exitHandler);
813
+ }
814
+ }
815
+ /**
816
+ * Enqueue an event for batched delivery.
817
+ *
818
+ * If the queue hits `flushThreshold`, an automatic flush is triggered.
819
+ * If the queue is full (`maxQueueSize`), the oldest event is dropped.
820
+ */
821
+ enqueue(method, url, body) {
822
+ if (this.shutdownCalled) return;
823
+ if (this.queue.length >= this.maxQueueSize) {
824
+ this.queue.shift();
825
+ if (typeof console !== "undefined") {
826
+ console.warn(
827
+ `Sentrial: Event queue full (${this.maxQueueSize}), dropping oldest event`
828
+ );
829
+ }
830
+ }
831
+ this.queue.push({ method, url, body });
832
+ if (this.queue.length >= this.flushThreshold) {
833
+ void this.flush();
834
+ }
835
+ }
836
+ /**
837
+ * Flush all queued events to the API.
838
+ *
839
+ * Drains the queue and fires all requests in parallel. Safe to call
840
+ * concurrently — only one flush runs at a time.
841
+ */
842
+ async flush() {
843
+ if (this.flushing || this.queue.length === 0) return;
844
+ this.flushing = true;
845
+ const batch = this.queue.splice(0, this.queue.length);
846
+ try {
847
+ await Promise.all(
848
+ batch.map(
849
+ (event) => this.sendFn(event.method, event.url, event.body).catch((err) => {
850
+ if (typeof console !== "undefined") {
851
+ console.warn("Sentrial: Batched event failed:", err);
852
+ }
853
+ })
854
+ )
855
+ );
856
+ } finally {
857
+ this.flushing = false;
858
+ }
859
+ }
860
+ /**
861
+ * Stop the batcher: clear the timer, flush remaining events, remove exit handler.
862
+ */
863
+ async shutdown() {
864
+ if (this.shutdownCalled) return;
865
+ this.shutdownCalled = true;
866
+ if (this.timer !== null) {
867
+ clearInterval(this.timer);
868
+ this.timer = null;
869
+ }
870
+ if (typeof process !== "undefined" && process.removeListener) {
871
+ process.removeListener("beforeExit", this.exitHandler);
872
+ }
873
+ this.flushing = false;
874
+ await this.flush();
875
+ }
876
+ /** Number of events currently queued. */
877
+ get size() {
878
+ return this.queue.length;
879
+ }
880
+ };
881
+
882
+ // src/types.ts
883
+ var EventType = /* @__PURE__ */ ((EventType2) => {
884
+ EventType2["TOOL_CALL"] = "tool_call";
885
+ EventType2["LLM_DECISION"] = "llm_decision";
886
+ EventType2["STATE_CHANGE"] = "state_change";
887
+ EventType2["ERROR"] = "error";
888
+ return EventType2;
889
+ })(EventType || {});
890
+
891
+ // src/client.ts
892
+ var DEFAULT_API_URL = "https://api.sentrial.com";
893
+ var MAX_RETRIES = 3;
894
+ var INITIAL_BACKOFF_MS = 500;
895
+ var MAX_BACKOFF_MS = 8e3;
896
+ var BACKOFF_MULTIPLIER = 2;
897
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
898
+ var REQUEST_TIMEOUT_MS = 1e4;
899
+ var SentrialClient = class {
900
+ apiUrl;
901
+ apiKey;
902
+ failSilently;
903
+ piiConfig;
904
+ piiConfigNeedsHydration = false;
905
+ piiHydrationPromise;
906
+ _stateVar = createContextVar({});
907
+ batcher;
908
+ /** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
909
+ sessionAccumulators = /* @__PURE__ */ new Map();
910
+ get currentState() {
911
+ return this._stateVar.get();
912
+ }
913
+ set currentState(value) {
914
+ this._stateVar.set(value);
915
+ }
916
+ constructor(config = {}) {
917
+ this.apiUrl = (config.apiUrl ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_URL : void 0) ?? DEFAULT_API_URL).replace(/\/$/, "");
918
+ this.apiKey = config.apiKey ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_KEY : void 0);
919
+ this.failSilently = config.failSilently ?? true;
920
+ if (config.pii === true) {
921
+ this.piiConfig = { enabled: true };
922
+ this.piiConfigNeedsHydration = true;
923
+ } else if (config.pii && typeof config.pii === "object") {
924
+ this.piiConfig = config.pii;
925
+ this.piiConfigNeedsHydration = false;
926
+ }
927
+ if (config.batching?.enabled) {
928
+ this.batcher = new EventBatcher(
929
+ (method, url, body) => this.safeRequest(method, url, body),
930
+ config.batching
931
+ );
932
+ }
933
+ }
934
+ /**
935
+ * Fetch the organization's PII config from the server.
936
+ *
937
+ * Called lazily on the first request when `pii: true` was passed to the constructor.
938
+ * Uses a single shared promise so concurrent requests don't trigger duplicate fetches.
939
+ */
940
+ async hydratePiiConfig() {
941
+ if (!this.piiConfigNeedsHydration) return;
942
+ if (this.piiHydrationPromise) {
943
+ await this.piiHydrationPromise;
944
+ return;
945
+ }
946
+ this.piiHydrationPromise = (async () => {
947
+ try {
948
+ const headers = {};
949
+ if (this.apiKey) {
950
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
951
+ }
952
+ const controller = new AbortController();
953
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
954
+ let response;
955
+ try {
956
+ response = await fetch(`${this.apiUrl}/api/sdk/pii-config`, {
957
+ method: "GET",
958
+ headers,
959
+ signal: controller.signal
960
+ });
961
+ } finally {
962
+ clearTimeout(timeoutId);
963
+ }
964
+ if (response.ok) {
965
+ const data = await response.json();
966
+ if (data.config) {
967
+ this.piiConfig = {
968
+ enabled: data.config.enabled,
969
+ mode: data.config.mode,
970
+ fields: data.config.fields,
971
+ builtinPatterns: data.config.builtinPatterns,
972
+ customPatterns: (data.config.customPatterns || []).map(
973
+ (cp) => ({
974
+ pattern: new RegExp(cp.pattern, "g"),
975
+ label: cp.label
976
+ })
977
+ ),
978
+ enhancedDetection: data.config.enhancedDetection
979
+ };
980
+ }
981
+ }
982
+ } catch {
983
+ }
984
+ this.piiConfigNeedsHydration = false;
985
+ })();
986
+ await this.piiHydrationPromise;
987
+ }
988
+ /**
989
+ * Make an HTTP request with retry logic and exponential backoff.
990
+ *
991
+ * Retries on transient failures (network errors, timeouts, 429/5xx).
992
+ * Up to MAX_RETRIES attempts with exponential backoff.
993
+ */
994
+ async safeRequest(method, url, body) {
995
+ if (this.piiConfigNeedsHydration) {
996
+ await this.hydratePiiConfig();
997
+ }
998
+ let lastError;
999
+ let backoff = INITIAL_BACKOFF_MS;
1000
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
1001
+ try {
1002
+ const headers = {
1003
+ "Content-Type": "application/json"
1004
+ };
1005
+ if (this.apiKey) {
1006
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1007
+ }
1008
+ const finalBody = this.piiConfig && body && typeof body === "object" ? redactPayload(body, this.piiConfig) : body;
1009
+ const controller = new AbortController();
1010
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1011
+ let response;
1012
+ try {
1013
+ response = await fetch(url, {
1014
+ method,
1015
+ headers,
1016
+ body: finalBody ? JSON.stringify(finalBody) : void 0,
1017
+ signal: controller.signal
1018
+ });
1019
+ } finally {
1020
+ clearTimeout(timeoutId);
1021
+ }
1022
+ if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
1023
+ await this.sleep(backoff);
1024
+ backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
1025
+ continue;
1026
+ }
1027
+ if (!response.ok) {
1028
+ const errorBody = await response.text();
1029
+ let errorData = {};
1030
+ try {
1031
+ errorData = JSON.parse(errorBody);
1032
+ } catch {
1033
+ }
1034
+ const error = new ApiError(
1035
+ errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
1036
+ response.status,
1037
+ errorData.error?.code
1038
+ );
1039
+ if (this.failSilently) {
1040
+ console.warn(`Sentrial: Request failed (${method} ${url}):`, error.message);
1041
+ return null;
1042
+ }
1043
+ throw error;
1044
+ }
1045
+ return await response.json();
1046
+ } catch (error) {
1047
+ if (error instanceof ApiError) {
1048
+ throw error;
1049
+ }
1050
+ lastError = error instanceof Error ? error : new Error(String(error));
1051
+ if (attempt < MAX_RETRIES) {
1052
+ await this.sleep(backoff);
1053
+ backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
1054
+ continue;
1055
+ }
1056
+ }
1057
+ }
1058
+ const networkError = new NetworkError(
1059
+ lastError?.message ?? "Unknown network error",
1060
+ lastError
1061
+ );
1062
+ if (this.failSilently) {
1063
+ console.warn(`Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`, networkError.message);
1064
+ return null;
1065
+ }
1066
+ throw networkError;
1067
+ }
1068
+ sleep(ms) {
1069
+ return new Promise((resolve) => setTimeout(resolve, ms));
1070
+ }
1071
+ accumulate(sessionId, cost, tokenCount, toolOutput) {
1072
+ let acc = this.sessionAccumulators.get(sessionId);
1073
+ if (!acc) {
1074
+ acc = { cost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1075
+ this.sessionAccumulators.set(sessionId, acc);
1076
+ }
1077
+ if (cost != null) acc.cost += cost;
1078
+ if (tokenCount != null) acc.totalTokens += tokenCount;
1079
+ const rawTokens = toolOutput?.tokens;
1080
+ if (rawTokens && typeof rawTokens === "object" && !Array.isArray(rawTokens)) {
1081
+ const tokens = rawTokens;
1082
+ if (typeof tokens.prompt === "number") acc.promptTokens += tokens.prompt;
1083
+ if (typeof tokens.completion === "number") acc.completionTokens += tokens.completion;
1084
+ }
412
1085
  }
413
1086
  /**
414
1087
  * Create a new session
@@ -444,6 +1117,7 @@ var SentrialClient = class {
444
1117
  * @returns Event data
445
1118
  */
446
1119
  async trackToolCall(params) {
1120
+ this.accumulate(params.sessionId, params.estimatedCost, params.tokenCount, params.toolOutput);
447
1121
  const stateBefore = { ...this.currentState };
448
1122
  this.currentState[`${params.toolName}_result`] = params.toolOutput;
449
1123
  const payload = {
@@ -462,6 +1136,10 @@ var SentrialClient = class {
462
1136
  if (params.traceId !== void 0) payload.traceId = params.traceId;
463
1137
  if (params.spanId !== void 0) payload.spanId = params.spanId;
464
1138
  if (params.metadata !== void 0) payload.metadata = params.metadata;
1139
+ if (this.batcher) {
1140
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1141
+ return null;
1142
+ }
465
1143
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
466
1144
  }
467
1145
  /**
@@ -471,6 +1149,7 @@ var SentrialClient = class {
471
1149
  * @returns Event data
472
1150
  */
473
1151
  async trackDecision(params) {
1152
+ this.accumulate(params.sessionId, params.estimatedCost, params.tokenCount);
474
1153
  const stateBefore = { ...this.currentState };
475
1154
  const payload = {
476
1155
  sessionId: params.sessionId,
@@ -486,6 +1165,10 @@ var SentrialClient = class {
486
1165
  if (params.traceId !== void 0) payload.traceId = params.traceId;
487
1166
  if (params.spanId !== void 0) payload.spanId = params.spanId;
488
1167
  if (params.metadata !== void 0) payload.metadata = params.metadata;
1168
+ if (this.batcher) {
1169
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1170
+ return null;
1171
+ }
489
1172
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
490
1173
  }
491
1174
  /**
@@ -512,6 +1195,10 @@ var SentrialClient = class {
512
1195
  if (params.traceId !== void 0) payload.traceId = params.traceId;
513
1196
  if (params.spanId !== void 0) payload.spanId = params.spanId;
514
1197
  if (params.metadata !== void 0) payload.metadata = params.metadata;
1198
+ if (this.batcher) {
1199
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1200
+ return null;
1201
+ }
515
1202
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
516
1203
  }
517
1204
  /**
@@ -557,6 +1244,10 @@ var SentrialClient = class {
557
1244
  if (params.metadata) {
558
1245
  payload.metadata = params.metadata;
559
1246
  }
1247
+ if (this.batcher) {
1248
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1249
+ return null;
1250
+ }
560
1251
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
561
1252
  }
562
1253
  /**
@@ -586,6 +1277,17 @@ var SentrialClient = class {
586
1277
  * ```
587
1278
  */
588
1279
  async completeSession(params) {
1280
+ if (this.batcher) {
1281
+ await this.batcher.flush();
1282
+ }
1283
+ const acc = this.sessionAccumulators.get(params.sessionId);
1284
+ if (acc) {
1285
+ if (params.estimatedCost === void 0 && acc.cost > 0) params = { ...params, estimatedCost: acc.cost };
1286
+ if (params.promptTokens === void 0 && acc.promptTokens > 0) params = { ...params, promptTokens: acc.promptTokens };
1287
+ if (params.completionTokens === void 0 && acc.completionTokens > 0) params = { ...params, completionTokens: acc.completionTokens };
1288
+ if (params.totalTokens === void 0 && acc.totalTokens > 0) params = { ...params, totalTokens: acc.totalTokens };
1289
+ this.sessionAccumulators.delete(params.sessionId);
1290
+ }
589
1291
  const payload = {
590
1292
  status: params.success !== false ? "completed" : "failed",
591
1293
  success: params.success ?? true
@@ -606,6 +1308,27 @@ var SentrialClient = class {
606
1308
  payload
607
1309
  );
608
1310
  }
1311
+ /**
1312
+ * Flush any queued events immediately.
1313
+ *
1314
+ * No-op if batching is not enabled.
1315
+ */
1316
+ async flush() {
1317
+ if (this.batcher) {
1318
+ await this.batcher.flush();
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Shut down the event batcher, flushing remaining events.
1323
+ *
1324
+ * Call this before your process exits for a clean shutdown.
1325
+ * No-op if batching is not enabled.
1326
+ */
1327
+ async shutdown() {
1328
+ if (this.batcher) {
1329
+ await this.batcher.shutdown();
1330
+ }
1331
+ }
609
1332
  /**
610
1333
  * Begin tracking an interaction (simplified API)
611
1334
  *
@@ -642,13 +1365,18 @@ var SentrialClient = class {
642
1365
  if (params.input) {
643
1366
  this.currentState.input = params.input;
644
1367
  }
1368
+ let sessionTokens;
1369
+ if (sessionId) {
1370
+ sessionTokens = _setSessionContextWithTokens(sessionId, this);
1371
+ }
645
1372
  return new Interaction({
646
1373
  client: this,
647
1374
  sessionId,
648
1375
  eventId,
649
1376
  userId: params.userId,
650
1377
  event: params.event,
651
- userInput: params.input
1378
+ userInput: params.input,
1379
+ sessionTokens
652
1380
  });
653
1381
  }
654
1382
  // Cost calculation static methods for convenience
@@ -665,12 +1393,15 @@ var Interaction = class {
665
1393
  userId;
666
1394
  /** Event name for this interaction */
667
1395
  event;
1396
+ startTime = Date.now();
668
1397
  finished = false;
669
1398
  success = true;
670
1399
  failureReason;
671
1400
  output;
672
1401
  userInput;
673
1402
  degraded;
1403
+ /** Context tokens for restoring previous session context on finish() */
1404
+ sessionTokens;
674
1405
  constructor(config) {
675
1406
  this.client = config.client;
676
1407
  this.sessionId = config.sessionId;
@@ -679,6 +1410,7 @@ var Interaction = class {
679
1410
  this.event = config.event;
680
1411
  this.userInput = config.userInput;
681
1412
  this.degraded = config.sessionId === null;
1413
+ this.sessionTokens = config.sessionTokens;
682
1414
  }
683
1415
  /**
684
1416
  * Set the output for this interaction
@@ -714,18 +1446,24 @@ var Interaction = class {
714
1446
  }
715
1447
  this.finished = true;
716
1448
  const finalOutput = params.output ?? this.output;
717
- return this.client.completeSession({
1449
+ const result = await this.client.completeSession({
718
1450
  sessionId: this.sessionId,
719
1451
  success: params.success ?? this.success,
720
1452
  failureReason: params.failureReason ?? this.failureReason,
721
1453
  estimatedCost: params.estimatedCost,
722
1454
  customMetrics: params.customMetrics,
1455
+ durationMs: params.durationMs ?? Date.now() - this.startTime,
723
1456
  promptTokens: params.promptTokens,
724
1457
  completionTokens: params.completionTokens,
725
1458
  totalTokens: params.totalTokens,
726
1459
  userInput: this.userInput,
727
1460
  assistantOutput: finalOutput
728
1461
  });
1462
+ if (this.sessionTokens) {
1463
+ _restoreSessionContext(this.sessionTokens);
1464
+ this.sessionTokens = void 0;
1465
+ }
1466
+ return result;
729
1467
  }
730
1468
  /**
731
1469
  * Track a tool call within this interaction
@@ -785,16 +1523,24 @@ function configure(config) {
785
1523
  function begin(params) {
786
1524
  return getClient().begin(params);
787
1525
  }
1526
+ async function flush() {
1527
+ if (defaultClient) await defaultClient.flush();
1528
+ }
1529
+ async function shutdown() {
1530
+ if (defaultClient) await defaultClient.shutdown();
1531
+ }
788
1532
  var sentrial = {
789
1533
  configure,
790
- begin
1534
+ begin,
1535
+ flush,
1536
+ shutdown
791
1537
  };
792
1538
 
793
1539
  // src/vercel.ts
794
- var _defaultClient = null;
1540
+ var _defaultClient2 = null;
795
1541
  var _globalConfig = {};
796
1542
  function configureVercel(config) {
797
- _defaultClient = new SentrialClient({
1543
+ _defaultClient2 = new SentrialClient({
798
1544
  apiKey: config.apiKey,
799
1545
  apiUrl: config.apiUrl,
800
1546
  failSilently: config.failSilently ?? true
@@ -806,10 +1552,10 @@ function configureVercel(config) {
806
1552
  };
807
1553
  }
808
1554
  function getClient2() {
809
- if (!_defaultClient) {
810
- _defaultClient = new SentrialClient();
1555
+ if (!_defaultClient2) {
1556
+ _defaultClient2 = new SentrialClient();
811
1557
  }
812
- return _defaultClient;
1558
+ return _defaultClient2;
813
1559
  }
814
1560
  function extractModelInfo(model) {
815
1561
  const modelId = model.modelId || model.id || "unknown";
@@ -818,7 +1564,7 @@ function extractModelInfo(model) {
818
1564
  }
819
1565
  function guessProvider(modelId) {
820
1566
  const id = modelId.toLowerCase();
821
- if (id.includes("gpt") || id.includes("o1") || id.includes("o3") || id.includes("o4") || id.startsWith("chatgpt")) return "openai";
1567
+ if (id.includes("gpt") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt")) return "openai";
822
1568
  if (id.includes("claude")) return "anthropic";
823
1569
  if (id.includes("gemini")) return "google";
824
1570
  if (id.includes("mistral") || id.includes("mixtral") || id.includes("codestral") || id.includes("pixtral")) return "mistral";
@@ -844,7 +1590,7 @@ function calculateCostForCall(provider, modelId, promptTokens, completionTokens)
844
1590
  case "mistral":
845
1591
  return promptTokens / 1e6 * 2 + completionTokens / 1e6 * 6;
846
1592
  default:
847
- return promptTokens * 3e-6 + completionTokens * 6e-6;
1593
+ return 0;
848
1594
  }
849
1595
  }
850
1596
  function extractInput(params) {
@@ -977,15 +1723,14 @@ function wrapGenerateText(originalFn, client, config) {
977
1723
  const result = await originalFn(wrappedParams);
978
1724
  const durationMs = Date.now() - startTime;
979
1725
  const resolvedModelId = result.response?.modelId || modelId;
980
- const promptTokens = result.usage?.promptTokens || 0;
981
- const completionTokens = result.usage?.completionTokens || 0;
982
- const totalTokens = result.usage?.totalTokens || promptTokens + completionTokens;
1726
+ const promptTokens = result.usage?.promptTokens ?? 0;
1727
+ const completionTokens = result.usage?.completionTokens ?? 0;
1728
+ const totalTokens = result.usage?.totalTokens ?? promptTokens + completionTokens;
983
1729
  const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
984
1730
  const steps = result.steps;
985
1731
  if (steps && steps.length >= 1) {
986
- for (let i = 0; i < steps.length; i++) {
987
- const step = steps[i];
988
- await client.trackEvent({
1732
+ const stepPromises = steps.map(
1733
+ (step, i) => client.trackEvent({
989
1734
  sessionId,
990
1735
  eventType: "llm_call",
991
1736
  eventData: {
@@ -993,14 +1738,16 @@ function wrapGenerateText(originalFn, client, config) {
993
1738
  provider,
994
1739
  step: i + 1,
995
1740
  total_steps: steps.length,
996
- prompt_tokens: step.usage?.promptTokens || 0,
997
- completion_tokens: step.usage?.completionTokens || 0,
998
- total_tokens: step.usage?.totalTokens || 0,
1741
+ prompt_tokens: step.usage?.promptTokens ?? 0,
1742
+ completion_tokens: step.usage?.completionTokens ?? 0,
1743
+ total_tokens: step.usage?.totalTokens ?? 0,
999
1744
  finish_reason: step.finishReason,
1000
1745
  tool_calls: step.toolCalls?.map((tc) => tc.toolName)
1001
1746
  }
1002
- });
1003
- }
1747
+ }).catch(() => {
1748
+ })
1749
+ );
1750
+ await Promise.all(stepPromises);
1004
1751
  } else {
1005
1752
  await client.trackEvent({
1006
1753
  sessionId,
@@ -1009,164 +1756,17 @@ function wrapGenerateText(originalFn, client, config) {
1009
1756
  model: resolvedModelId,
1010
1757
  provider,
1011
1758
  prompt_tokens: promptTokens,
1012
- completion_tokens: completionTokens,
1013
- total_tokens: totalTokens,
1014
- finish_reason: result.finishReason,
1015
- tool_calls: result.toolCalls?.map((tc) => tc.toolName)
1016
- }
1017
- });
1018
- }
1019
- await client.completeSession({
1020
- sessionId,
1021
- success: true,
1022
- output: result.text,
1023
- durationMs,
1024
- estimatedCost: cost,
1025
- promptTokens,
1026
- completionTokens,
1027
- totalTokens
1028
- });
1029
- return result;
1030
- } catch (error) {
1031
- const durationMs = Date.now() - startTime;
1032
- await client.trackError({
1033
- sessionId,
1034
- errorType: error instanceof Error ? error.name : "Error",
1035
- errorMessage: error instanceof Error ? error.message : "Unknown error"
1036
- });
1037
- await client.completeSession({
1038
- sessionId,
1039
- success: false,
1040
- failureReason: error instanceof Error ? error.message : "Unknown error",
1041
- durationMs
1042
- });
1043
- throw error;
1044
- }
1045
- };
1046
- }
1047
- function wrapStreamText(originalFn, client, config) {
1048
- return (params) => {
1049
- const startTime = Date.now();
1050
- const { modelId, provider } = extractModelInfo(params.model);
1051
- const input = extractInput(params);
1052
- let sessionId = null;
1053
- const sessionPromise = (async () => {
1054
- try {
1055
- const id = await client.createSession({
1056
- name: `streamText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1057
- agentName: config.defaultAgent ?? "vercel-ai-sdk",
1058
- userId: config.userId ?? "anonymous",
1059
- convoId: config.convoId,
1060
- metadata: {
1061
- model: modelId,
1062
- provider,
1063
- function: "streamText"
1064
- }
1065
- });
1066
- sessionId = id;
1067
- if (id) {
1068
- client.setInput(id, input).catch(() => {
1069
- });
1070
- }
1071
- return id;
1072
- } catch {
1073
- return null;
1074
- }
1075
- })();
1076
- let tracked = false;
1077
- async function trackCompletion(fullText, usageInfo, error) {
1078
- if (tracked) return;
1079
- tracked = true;
1080
- const durationMs = Date.now() - startTime;
1081
- const sid = sessionId || await sessionPromise;
1082
- if (!sid) return;
1083
- if (error) {
1084
- await client.trackError({
1085
- sessionId: sid,
1086
- errorType: error.name || "Error",
1087
- errorMessage: error.message || "Unknown error"
1088
- });
1089
- await client.completeSession({
1090
- sessionId: sid,
1091
- success: false,
1092
- failureReason: error.message || "Unknown error",
1093
- durationMs
1094
- });
1095
- return;
1096
- }
1097
- const promptTokens = usageInfo?.promptTokens || 0;
1098
- const completionTokens = usageInfo?.completionTokens || 0;
1099
- const totalTokens = usageInfo?.totalTokens || promptTokens + completionTokens;
1100
- const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
1101
- await client.completeSession({
1102
- sessionId: sid,
1103
- success: true,
1104
- output: fullText,
1105
- durationMs,
1106
- estimatedCost: cost,
1107
- promptTokens,
1108
- completionTokens,
1109
- totalTokens
1110
- });
1111
- }
1112
- const userOnFinish = params.onFinish;
1113
- const wrappedParams = {
1114
- ...params,
1115
- tools: params.tools ? wrapToolsAsync(params.tools, sessionPromise, client) : void 0,
1116
- onFinish: async (event) => {
1117
- try {
1118
- if (userOnFinish) await userOnFinish(event);
1119
- } catch {
1120
- }
1121
- await trackCompletion(event.text, event.usage);
1122
- }
1123
- };
1124
- const result = originalFn(wrappedParams);
1125
- const textProp = result.text;
1126
- if (textProp != null && typeof textProp.then === "function") {
1127
- textProp.then((text) => {
1128
- trackCompletion(text).catch(() => {
1129
- });
1130
- }).catch((err) => {
1131
- trackCompletion("", void 0, err instanceof Error ? err : new Error(String(err))).catch(() => {
1132
- });
1133
- });
1134
- }
1135
- return result;
1136
- };
1137
- }
1138
- function wrapGenerateObject(originalFn, client, config) {
1139
- return async (params) => {
1140
- const startTime = Date.now();
1141
- const { modelId, provider } = extractModelInfo(params.model);
1142
- const input = extractInput(params);
1143
- const sessionId = await client.createSession({
1144
- name: `generateObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1145
- agentName: config.defaultAgent ?? "vercel-ai-sdk",
1146
- userId: config.userId ?? "anonymous",
1147
- convoId: config.convoId,
1148
- metadata: {
1149
- model: modelId,
1150
- provider,
1151
- function: "generateObject"
1152
- }
1153
- });
1154
- if (!sessionId) {
1155
- return originalFn(params);
1156
- }
1157
- await client.setInput(sessionId, input);
1158
- try {
1159
- const result = await originalFn(params);
1160
- const durationMs = Date.now() - startTime;
1161
- const resolvedModelId = result.response?.modelId || modelId;
1162
- const promptTokens = result.usage?.promptTokens || 0;
1163
- const completionTokens = result.usage?.completionTokens || 0;
1164
- const totalTokens = result.usage?.totalTokens || promptTokens + completionTokens;
1165
- const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
1759
+ completion_tokens: completionTokens,
1760
+ total_tokens: totalTokens,
1761
+ finish_reason: result.finishReason,
1762
+ tool_calls: result.toolCalls?.map((tc) => tc.toolName)
1763
+ }
1764
+ });
1765
+ }
1166
1766
  await client.completeSession({
1167
1767
  sessionId,
1168
1768
  success: true,
1169
- output: JSON.stringify(result.object),
1769
+ output: result.text,
1170
1770
  durationMs,
1171
1771
  estimatedCost: cost,
1172
1772
  promptTokens,
@@ -1191,24 +1791,26 @@ function wrapGenerateObject(originalFn, client, config) {
1191
1791
  }
1192
1792
  };
1193
1793
  }
1194
- function wrapStreamObject(originalFn, client, config) {
1794
+ function wrapStreamText(originalFn, client, config) {
1195
1795
  return (params) => {
1196
1796
  const startTime = Date.now();
1197
1797
  const { modelId, provider } = extractModelInfo(params.model);
1198
1798
  const input = extractInput(params);
1799
+ let sessionId = null;
1199
1800
  const sessionPromise = (async () => {
1200
1801
  try {
1201
1802
  const id = await client.createSession({
1202
- name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1803
+ name: `streamText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1203
1804
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
1204
1805
  userId: config.userId ?? "anonymous",
1205
1806
  convoId: config.convoId,
1206
1807
  metadata: {
1207
1808
  model: modelId,
1208
1809
  provider,
1209
- function: "streamObject"
1810
+ function: "streamText"
1210
1811
  }
1211
1812
  });
1813
+ sessionId = id;
1212
1814
  if (id) {
1213
1815
  client.setInput(id, input).catch(() => {
1214
1816
  });
@@ -1218,364 +1820,481 @@ function wrapStreamObject(originalFn, client, config) {
1218
1820
  return null;
1219
1821
  }
1220
1822
  })();
1221
- let tracked = false;
1222
- const userOnFinish = params.onFinish;
1223
1823
  const wrappedParams = {
1224
1824
  ...params,
1225
- onFinish: async (event) => {
1226
- try {
1227
- if (userOnFinish) await userOnFinish(event);
1228
- } catch {
1229
- }
1230
- if (tracked) return;
1231
- tracked = true;
1232
- const durationMs = Date.now() - startTime;
1233
- const sid = await sessionPromise;
1234
- if (!sid) return;
1235
- if (event.error) {
1236
- const err = event.error instanceof Error ? event.error : new Error(String(event.error));
1237
- await client.trackError({
1238
- sessionId: sid,
1239
- errorType: err.name || "Error",
1240
- errorMessage: err.message || "Unknown error"
1241
- });
1242
- await client.completeSession({
1825
+ tools: params.tools ? wrapToolsAsync(params.tools, sessionPromise, client) : void 0
1826
+ };
1827
+ const result = originalFn(wrappedParams);
1828
+ const originalTextStream = result.textStream;
1829
+ let fullText = "";
1830
+ let tracked = false;
1831
+ async function trackCompletion(text, error) {
1832
+ if (tracked) return;
1833
+ tracked = true;
1834
+ const durationMs = Date.now() - startTime;
1835
+ const sid = sessionId || await sessionPromise;
1836
+ if (!sid) return;
1837
+ if (error) {
1838
+ await client.trackError({
1839
+ sessionId: sid,
1840
+ errorType: error.name || "Error",
1841
+ errorMessage: error.message || "Unknown error"
1842
+ }).catch(() => {
1843
+ });
1844
+ await client.completeSession({
1845
+ sessionId: sid,
1846
+ success: false,
1847
+ failureReason: error.message || "Unknown error",
1848
+ durationMs
1849
+ }).catch(() => {
1850
+ });
1851
+ return;
1852
+ }
1853
+ let resolvedModelId = modelId;
1854
+ try {
1855
+ const resp = result.response ? await result.response : void 0;
1856
+ if (resp?.modelId) resolvedModelId = resp.modelId;
1857
+ } catch {
1858
+ }
1859
+ let usage;
1860
+ try {
1861
+ usage = result.usage ? await result.usage : void 0;
1862
+ } catch {
1863
+ }
1864
+ let steps;
1865
+ try {
1866
+ steps = result.steps ? await result.steps : void 0;
1867
+ } catch {
1868
+ }
1869
+ if (steps && steps.length >= 1) {
1870
+ let totalPrompt = 0, totalCompletion = 0;
1871
+ const stepPromises = steps.map((step, i) => {
1872
+ const sp = step.usage?.promptTokens ?? 0;
1873
+ const sc = step.usage?.completionTokens ?? 0;
1874
+ totalPrompt += sp;
1875
+ totalCompletion += sc;
1876
+ return client.trackEvent({
1243
1877
  sessionId: sid,
1244
- success: false,
1245
- failureReason: err.message || "Unknown error",
1246
- durationMs
1878
+ eventType: "llm_call",
1879
+ eventData: {
1880
+ model: resolvedModelId,
1881
+ provider,
1882
+ step: i + 1,
1883
+ total_steps: steps.length,
1884
+ prompt_tokens: sp,
1885
+ completion_tokens: sc,
1886
+ total_tokens: step.usage?.totalTokens ?? 0,
1887
+ finish_reason: step.finishReason,
1888
+ tool_calls: step.toolCalls?.map((tc) => tc.toolName)
1889
+ }
1890
+ }).catch(() => {
1247
1891
  });
1248
- return;
1249
- }
1250
- const promptTokens = event.usage?.promptTokens || 0;
1251
- const completionTokens = event.usage?.completionTokens || 0;
1252
- const totalTokens = event.usage?.totalTokens || promptTokens + completionTokens;
1253
- const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
1892
+ });
1893
+ await Promise.all(stepPromises);
1894
+ const promptTokens = usage?.promptTokens ?? totalPrompt;
1895
+ const completionTokens = usage?.completionTokens ?? totalCompletion;
1896
+ const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
1897
+ const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
1898
+ await client.completeSession({
1899
+ sessionId: sid,
1900
+ success: true,
1901
+ output: text,
1902
+ durationMs,
1903
+ estimatedCost: cost,
1904
+ promptTokens,
1905
+ completionTokens,
1906
+ totalTokens
1907
+ }).catch(() => {
1908
+ });
1909
+ } else {
1910
+ const promptTokens = usage?.promptTokens ?? 0;
1911
+ const completionTokens = usage?.completionTokens ?? 0;
1912
+ const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
1913
+ const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
1914
+ await client.trackEvent({
1915
+ sessionId: sid,
1916
+ eventType: "llm_call",
1917
+ eventData: {
1918
+ model: resolvedModelId,
1919
+ provider,
1920
+ prompt_tokens: promptTokens,
1921
+ completion_tokens: completionTokens,
1922
+ total_tokens: totalTokens
1923
+ }
1924
+ }).catch(() => {
1925
+ });
1254
1926
  await client.completeSession({
1255
1927
  sessionId: sid,
1256
1928
  success: true,
1257
- output: event.object != null ? JSON.stringify(event.object) : "",
1929
+ output: text,
1258
1930
  durationMs,
1259
1931
  estimatedCost: cost,
1260
1932
  promptTokens,
1261
1933
  completionTokens,
1262
1934
  totalTokens
1935
+ }).catch(() => {
1263
1936
  });
1264
1937
  }
1265
- };
1266
- return originalFn(wrappedParams);
1267
- };
1268
- }
1269
- function wrapAISDK(ai, options) {
1270
- const client = options?.client ?? getClient2();
1271
- const config = {
1272
- defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
1273
- userId: options?.userId ?? _globalConfig.userId,
1274
- convoId: options?.convoId ?? _globalConfig.convoId
1275
- };
1276
- return {
1277
- generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
1278
- () => Promise.reject(new Error("generateText not available")),
1279
- client,
1280
- config
1281
- ),
1282
- streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
1283
- })() }), client, config),
1284
- generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
1285
- () => Promise.reject(new Error("generateObject not available")),
1286
- client,
1287
- config
1288
- ),
1289
- streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
1938
+ }
1939
+ result.textStream = (async function* () {
1940
+ try {
1941
+ for await (const chunk of originalTextStream) {
1942
+ fullText += chunk;
1943
+ yield chunk;
1944
+ }
1945
+ await trackCompletion(fullText);
1946
+ } catch (error) {
1947
+ await trackCompletion(
1948
+ fullText,
1949
+ error instanceof Error ? error : new Error(String(error))
1950
+ );
1951
+ throw error;
1952
+ }
1953
+ })();
1954
+ return result;
1290
1955
  };
1291
1956
  }
1292
-
1293
- // src/wrappers.ts
1294
- var _currentSessionId = null;
1295
- var _currentClient = null;
1296
- var _defaultClient2 = null;
1297
- function setSessionContext(sessionId, client) {
1298
- _currentSessionId = sessionId;
1299
- if (client) {
1300
- _currentClient = client;
1301
- }
1302
- }
1303
- function clearSessionContext() {
1304
- _currentSessionId = null;
1305
- _currentClient = null;
1306
- }
1307
- function getSessionContext() {
1308
- return _currentSessionId;
1309
- }
1310
- function setDefaultClient(client) {
1311
- _defaultClient2 = client;
1312
- }
1313
- function getTrackingClient() {
1314
- return _currentClient ?? _defaultClient2;
1315
- }
1316
- function wrapOpenAI(client, options = {}) {
1317
- const { trackWithoutSession = false } = options;
1318
- const chat = client.chat;
1319
- if (!chat?.completions?.create) {
1320
- console.warn("Sentrial: OpenAI client does not have chat.completions.create");
1321
- return client;
1322
- }
1323
- const originalCreate = chat.completions.create.bind(chat.completions);
1324
- chat.completions.create = async function(...args) {
1957
+ function wrapGenerateObject(originalFn, client, config) {
1958
+ return async (params) => {
1325
1959
  const startTime = Date.now();
1326
- const params = args[0] ?? {};
1327
- const messages = params.messages ?? [];
1328
- const model = params.model ?? "unknown";
1329
- try {
1330
- const response = await originalCreate(...args);
1331
- const durationMs = Date.now() - startTime;
1332
- const promptTokens = response.usage?.prompt_tokens ?? 0;
1333
- const completionTokens = response.usage?.completion_tokens ?? 0;
1334
- const totalTokens = response.usage?.total_tokens ?? 0;
1335
- let outputContent = "";
1336
- if (response.choices?.[0]?.message?.content) {
1337
- outputContent = response.choices[0].message.content;
1960
+ const { modelId, provider } = extractModelInfo(params.model);
1961
+ const input = extractInput(params);
1962
+ const sessionId = await client.createSession({
1963
+ name: `generateObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1964
+ agentName: config.defaultAgent ?? "vercel-ai-sdk",
1965
+ userId: config.userId ?? "anonymous",
1966
+ convoId: config.convoId,
1967
+ metadata: {
1968
+ model: modelId,
1969
+ provider,
1970
+ function: "generateObject"
1338
1971
  }
1339
- const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
1340
- trackLLMCall({
1341
- provider: "openai",
1342
- model,
1343
- messages,
1344
- output: outputContent,
1345
- promptTokens,
1346
- completionTokens,
1347
- totalTokens,
1348
- cost,
1349
- durationMs,
1350
- trackWithoutSession
1351
- });
1352
- return response;
1353
- } catch (error) {
1354
- const durationMs = Date.now() - startTime;
1355
- trackLLMError({
1356
- provider: "openai",
1357
- model,
1358
- messages,
1359
- error,
1360
- durationMs,
1361
- trackWithoutSession
1362
- });
1363
- throw error;
1972
+ });
1973
+ if (!sessionId) {
1974
+ return originalFn(params);
1364
1975
  }
1365
- };
1366
- return client;
1367
- }
1368
- function wrapAnthropic(client, options = {}) {
1369
- const { trackWithoutSession = false } = options;
1370
- const messages = client.messages;
1371
- if (!messages?.create) {
1372
- console.warn("Sentrial: Anthropic client does not have messages.create");
1373
- return client;
1374
- }
1375
- const originalCreate = messages.create.bind(messages);
1376
- messages.create = async function(...args) {
1377
- const startTime = Date.now();
1378
- const params = args[0] ?? {};
1379
- const inputMessages = params.messages ?? [];
1380
- const model = params.model ?? "unknown";
1381
- const system = params.system ?? "";
1976
+ await client.setInput(sessionId, input);
1382
1977
  try {
1383
- const response = await originalCreate(...args);
1978
+ const result = await originalFn(params);
1384
1979
  const durationMs = Date.now() - startTime;
1385
- const promptTokens = response.usage?.input_tokens ?? 0;
1386
- const completionTokens = response.usage?.output_tokens ?? 0;
1387
- const totalTokens = promptTokens + completionTokens;
1388
- let outputContent = "";
1389
- if (response.content) {
1390
- for (const block of response.content) {
1391
- if (block.type === "text") {
1392
- outputContent += block.text;
1393
- }
1980
+ const resolvedModelId = result.response?.modelId || modelId;
1981
+ const promptTokens = result.usage?.promptTokens ?? 0;
1982
+ const completionTokens = result.usage?.completionTokens ?? 0;
1983
+ const totalTokens = result.usage?.totalTokens ?? promptTokens + completionTokens;
1984
+ const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
1985
+ await client.trackEvent({
1986
+ sessionId,
1987
+ eventType: "llm_call",
1988
+ eventData: {
1989
+ model: resolvedModelId,
1990
+ provider,
1991
+ prompt_tokens: promptTokens,
1992
+ completion_tokens: completionTokens,
1993
+ total_tokens: totalTokens
1394
1994
  }
1395
- }
1396
- const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
1397
- const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
1398
- trackLLMCall({
1399
- provider: "anthropic",
1400
- model,
1401
- messages: fullMessages,
1402
- output: outputContent,
1995
+ }).catch(() => {
1996
+ });
1997
+ await client.completeSession({
1998
+ sessionId,
1999
+ success: true,
2000
+ output: JSON.stringify(result.object),
2001
+ durationMs,
2002
+ estimatedCost: cost,
1403
2003
  promptTokens,
1404
2004
  completionTokens,
1405
- totalTokens,
1406
- cost,
1407
- durationMs,
1408
- trackWithoutSession
2005
+ totalTokens
2006
+ });
2007
+ return result;
2008
+ } catch (error) {
2009
+ const durationMs = Date.now() - startTime;
2010
+ await client.trackError({
2011
+ sessionId,
2012
+ errorType: error instanceof Error ? error.name : "Error",
2013
+ errorMessage: error instanceof Error ? error.message : "Unknown error"
1409
2014
  });
1410
- return response;
1411
- } catch (error) {
1412
- const durationMs = Date.now() - startTime;
1413
- trackLLMError({
1414
- provider: "anthropic",
1415
- model,
1416
- messages: inputMessages,
1417
- error,
1418
- durationMs,
1419
- trackWithoutSession
2015
+ await client.completeSession({
2016
+ sessionId,
2017
+ success: false,
2018
+ failureReason: error instanceof Error ? error.message : "Unknown error",
2019
+ durationMs
1420
2020
  });
1421
2021
  throw error;
1422
2022
  }
1423
2023
  };
1424
- return client;
1425
2024
  }
1426
- function wrapGoogle(model, options = {}) {
1427
- const { trackWithoutSession = false } = options;
1428
- const originalGenerate = model.generateContent;
1429
- if (!originalGenerate) {
1430
- console.warn("Sentrial: Google model does not have generateContent");
1431
- return model;
1432
- }
1433
- model.generateContent = async function(...args) {
2025
+ function wrapStreamObject(originalFn, client, config) {
2026
+ return (params) => {
1434
2027
  const startTime = Date.now();
1435
- const contents = args[0];
1436
- const modelName = model.model ?? "gemini-unknown";
1437
- const messages = googleContentsToMessages(contents);
1438
- try {
1439
- const response = await originalGenerate.apply(model, args);
2028
+ const { modelId, provider } = extractModelInfo(params.model);
2029
+ const input = extractInput(params);
2030
+ const sessionPromise = (async () => {
2031
+ try {
2032
+ const id = await client.createSession({
2033
+ name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
2034
+ agentName: config.defaultAgent ?? "vercel-ai-sdk",
2035
+ userId: config.userId ?? "anonymous",
2036
+ convoId: config.convoId,
2037
+ metadata: {
2038
+ model: modelId,
2039
+ provider,
2040
+ function: "streamObject"
2041
+ }
2042
+ });
2043
+ if (id) {
2044
+ client.setInput(id, input).catch(() => {
2045
+ });
2046
+ }
2047
+ return id;
2048
+ } catch {
2049
+ return null;
2050
+ }
2051
+ })();
2052
+ const result = originalFn(params);
2053
+ async function completeStreamObject(obj, error) {
1440
2054
  const durationMs = Date.now() - startTime;
1441
- let promptTokens = 0;
1442
- let completionTokens = 0;
1443
- if (response.usageMetadata) {
1444
- promptTokens = response.usageMetadata.promptTokenCount ?? 0;
1445
- completionTokens = response.usageMetadata.candidatesTokenCount ?? 0;
2055
+ const sid = await sessionPromise;
2056
+ if (!sid) return;
2057
+ if (error) {
2058
+ await client.trackError({
2059
+ sessionId: sid,
2060
+ errorType: error.name || "Error",
2061
+ errorMessage: error.message || "Unknown error"
2062
+ }).catch(() => {
2063
+ });
2064
+ await client.completeSession({
2065
+ sessionId: sid,
2066
+ success: false,
2067
+ failureReason: error.message || "Unknown error",
2068
+ durationMs
2069
+ }).catch(() => {
2070
+ });
2071
+ return;
1446
2072
  }
1447
- const totalTokens = promptTokens + completionTokens;
1448
- let outputContent = "";
2073
+ let usage;
1449
2074
  try {
1450
- outputContent = response.response?.text() ?? "";
2075
+ usage = result.usage ? await result.usage : void 0;
1451
2076
  } catch {
1452
2077
  }
1453
- const cost = calculateGoogleCost({ model: modelName, inputTokens: promptTokens, outputTokens: completionTokens });
1454
- trackLLMCall({
1455
- provider: "google",
1456
- model: modelName,
1457
- messages,
1458
- output: outputContent,
2078
+ const promptTokens = usage?.promptTokens ?? 0;
2079
+ const completionTokens = usage?.completionTokens ?? 0;
2080
+ const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
2081
+ const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
2082
+ await client.trackEvent({
2083
+ sessionId: sid,
2084
+ eventType: "llm_call",
2085
+ eventData: {
2086
+ model: modelId,
2087
+ provider,
2088
+ prompt_tokens: promptTokens,
2089
+ completion_tokens: completionTokens,
2090
+ total_tokens: totalTokens
2091
+ }
2092
+ }).catch(() => {
2093
+ });
2094
+ await client.completeSession({
2095
+ sessionId: sid,
2096
+ success: true,
2097
+ output: JSON.stringify(obj),
2098
+ durationMs,
2099
+ estimatedCost: cost,
1459
2100
  promptTokens,
1460
2101
  completionTokens,
1461
- totalTokens,
1462
- cost,
1463
- durationMs,
1464
- trackWithoutSession
2102
+ totalTokens
2103
+ }).catch(() => {
1465
2104
  });
1466
- return response;
1467
- } catch (error) {
1468
- const durationMs = Date.now() - startTime;
1469
- trackLLMError({
1470
- provider: "google",
1471
- model: modelName,
1472
- messages,
1473
- error,
1474
- durationMs,
1475
- trackWithoutSession
2105
+ }
2106
+ if (result.object) {
2107
+ const originalObjectPromise = result.object;
2108
+ result.object = originalObjectPromise.then(async (obj) => {
2109
+ await completeStreamObject(obj);
2110
+ return obj;
2111
+ }).catch(async (error) => {
2112
+ await completeStreamObject(void 0, error instanceof Error ? error : new Error(String(error)));
2113
+ throw error;
2114
+ });
2115
+ } else if (result.usage) {
2116
+ result.usage.then(async () => {
2117
+ await completeStreamObject(void 0);
2118
+ }).catch(async (error) => {
2119
+ await completeStreamObject(void 0, error instanceof Error ? error : new Error(String(error)));
1476
2120
  });
1477
- throw error;
1478
2121
  }
2122
+ return result;
1479
2123
  };
1480
- return model;
1481
- }
1482
- function googleContentsToMessages(contents) {
1483
- if (typeof contents === "string") {
1484
- return [{ role: "user", content: contents }];
1485
- }
1486
- if (Array.isArray(contents)) {
1487
- return contents.map((item) => {
1488
- if (typeof item === "string") {
1489
- return { role: "user", content: item };
1490
- }
1491
- if (item && typeof item === "object") {
1492
- return { role: item.role ?? "user", content: String(item.content ?? item) };
1493
- }
1494
- return { role: "user", content: String(item) };
1495
- });
1496
- }
1497
- return [{ role: "user", content: String(contents) }];
1498
2124
  }
1499
- function wrapLLM(client, provider) {
1500
- if (provider === "openai" || client.chat?.completions?.create) {
1501
- return wrapOpenAI(client);
1502
- }
1503
- if (provider === "anthropic" || client.messages?.create) {
1504
- return wrapAnthropic(client);
1505
- }
1506
- if (provider === "google" || client.generateContent) {
1507
- return wrapGoogle(client);
1508
- }
1509
- console.warn("Sentrial: Unknown LLM client type. No auto-tracking applied.");
1510
- return client;
2125
+ function wrapAISDK(ai, options) {
2126
+ const client = options?.client ?? getClient2();
2127
+ const config = {
2128
+ defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
2129
+ userId: options?.userId ?? _globalConfig.userId,
2130
+ convoId: options?.convoId ?? _globalConfig.convoId
2131
+ };
2132
+ return {
2133
+ generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
2134
+ () => Promise.reject(new Error("generateText not available")),
2135
+ client,
2136
+ config
2137
+ ),
2138
+ streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
2139
+ })() }), client, config),
2140
+ generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
2141
+ () => Promise.reject(new Error("generateObject not available")),
2142
+ client,
2143
+ config
2144
+ ),
2145
+ streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
2146
+ };
1511
2147
  }
1512
- function trackLLMCall(params) {
1513
- const client = getTrackingClient();
1514
- if (!client) return;
1515
- const sessionId = _currentSessionId;
1516
- if (!sessionId && !params.trackWithoutSession) {
1517
- return;
1518
- }
1519
- if (sessionId) {
1520
- client.trackToolCall({
1521
- sessionId,
1522
- toolName: `llm:${params.provider}:${params.model}`,
1523
- toolInput: {
1524
- messages: params.messages,
1525
- model: params.model,
1526
- provider: params.provider
1527
- },
1528
- toolOutput: {
1529
- content: params.output,
1530
- tokens: {
1531
- prompt: params.promptTokens,
1532
- completion: params.completionTokens,
1533
- total: params.totalTokens
1534
- },
1535
- cost_usd: params.cost
1536
- },
1537
- reasoning: `LLM call to ${params.provider} ${params.model}`,
1538
- estimatedCost: params.cost,
1539
- tokenCount: params.totalTokens,
1540
- metadata: {
1541
- provider: params.provider,
1542
- model: params.model,
1543
- duration_ms: params.durationMs,
1544
- prompt_tokens: params.promptTokens,
1545
- completion_tokens: params.completionTokens
1546
- }
1547
- }).catch((err) => {
1548
- console.warn("Sentrial: Failed to track LLM call:", err.message);
2148
+
2149
+ // src/claude-code.ts
2150
+ function wrapClaudeAgent(queryFn, wrapOptions) {
2151
+ const {
2152
+ client,
2153
+ defaultAgent = "claude-agent",
2154
+ userId = "anonymous",
2155
+ convoId,
2156
+ extraMetadata
2157
+ } = wrapOptions;
2158
+ return function wrappedQuery(params) {
2159
+ const { prompt, options = {} } = params;
2160
+ const startTime = Date.now();
2161
+ let sessionId = null;
2162
+ let resolveSessionReady;
2163
+ const sessionReady = new Promise((resolve) => {
2164
+ resolveSessionReady = resolve;
1549
2165
  });
1550
- }
1551
- }
1552
- function trackLLMError(params) {
1553
- const client = getTrackingClient();
1554
- if (!client) return;
1555
- const sessionId = _currentSessionId;
1556
- if (!sessionId && !params.trackWithoutSession) {
1557
- return;
1558
- }
1559
- if (sessionId) {
1560
- client.trackError({
1561
- sessionId,
1562
- errorMessage: params.error.message,
1563
- errorType: params.error.name,
1564
- toolName: `llm:${params.provider}:${params.model}`,
1565
- metadata: {
1566
- provider: params.provider,
1567
- model: params.model,
1568
- duration_ms: params.durationMs
2166
+ const sessionName = typeof prompt === "string" ? `${defaultAgent}: ${prompt.slice(0, 100)}` : `${defaultAgent} session`;
2167
+ const pendingToolCalls = [];
2168
+ const sentrialToolHook = {
2169
+ hooks: [
2170
+ async (input, toolUseID, _opts) => {
2171
+ await sessionReady;
2172
+ if (!sessionId) return;
2173
+ const toolOutput = input?.tool_response && typeof input.tool_response === "object" ? input.tool_response : { response: input?.tool_response ?? null };
2174
+ const p = client.trackToolCall({
2175
+ sessionId,
2176
+ toolName: input?.tool_name ?? "unknown",
2177
+ toolInput: input?.tool_input ?? {},
2178
+ toolOutput,
2179
+ metadata: { tool_use_id: toolUseID }
2180
+ }).catch(() => {
2181
+ });
2182
+ pendingToolCalls.push(p);
2183
+ }
2184
+ ]
2185
+ };
2186
+ const sentrialToolFailureHook = {
2187
+ hooks: [
2188
+ async (input, toolUseID, _opts) => {
2189
+ await sessionReady;
2190
+ if (!sessionId) return;
2191
+ const p = client.trackToolCall({
2192
+ sessionId,
2193
+ toolName: input?.tool_name ?? "unknown",
2194
+ toolInput: input?.tool_input ?? {},
2195
+ toolOutput: {},
2196
+ toolError: { message: input?.error ?? "unknown error" },
2197
+ metadata: { tool_use_id: toolUseID }
2198
+ }).catch(() => {
2199
+ });
2200
+ pendingToolCalls.push(p);
2201
+ }
2202
+ ]
2203
+ };
2204
+ const mergedHooks = {
2205
+ ...options.hooks ?? {}
2206
+ };
2207
+ const existingPostToolUse = mergedHooks.PostToolUse ?? [];
2208
+ mergedHooks.PostToolUse = [...existingPostToolUse, sentrialToolHook];
2209
+ const existingPostToolUseFailure = mergedHooks.PostToolUseFailure ?? [];
2210
+ mergedHooks.PostToolUseFailure = [...existingPostToolUseFailure, sentrialToolFailureHook];
2211
+ const mergedOptions = {
2212
+ ...options,
2213
+ hooks: mergedHooks
2214
+ };
2215
+ const generator = queryFn({ prompt, options: mergedOptions });
2216
+ return (async function* () {
2217
+ try {
2218
+ for await (const message of generator) {
2219
+ if (message.type === "system" && message.subtype === "init") {
2220
+ const metadata = {
2221
+ model: message.model,
2222
+ tools: message.tools,
2223
+ cwd: message.cwd,
2224
+ mcp_servers: message.mcp_servers,
2225
+ sdk_session_id: message.session_id,
2226
+ ...extraMetadata ?? {}
2227
+ };
2228
+ try {
2229
+ sessionId = await client.createSession({
2230
+ name: sessionName,
2231
+ agentName: defaultAgent,
2232
+ userId,
2233
+ convoId,
2234
+ metadata
2235
+ });
2236
+ } catch {
2237
+ sessionId = null;
2238
+ }
2239
+ resolveSessionReady();
2240
+ }
2241
+ if (message.type === "result" && sessionId) {
2242
+ const isError = !!message.is_error;
2243
+ const inputTokens = message.usage?.input_tokens ?? 0;
2244
+ const outputTokens = message.usage?.output_tokens ?? 0;
2245
+ let failureReason;
2246
+ if (isError) {
2247
+ if (message.errors && message.errors.length > 0) {
2248
+ failureReason = message.errors.join("; ");
2249
+ } else {
2250
+ failureReason = message.subtype;
2251
+ }
2252
+ }
2253
+ await Promise.allSettled(pendingToolCalls);
2254
+ try {
2255
+ await client.completeSession({
2256
+ sessionId,
2257
+ success: !isError,
2258
+ failureReason,
2259
+ estimatedCost: message.total_cost_usd,
2260
+ promptTokens: inputTokens,
2261
+ completionTokens: outputTokens,
2262
+ totalTokens: inputTokens + outputTokens,
2263
+ durationMs: message.duration_ms ?? Date.now() - startTime,
2264
+ userInput: typeof prompt === "string" ? prompt : void 0,
2265
+ output: message.result,
2266
+ customMetrics: {
2267
+ num_turns: message.num_turns ?? 0,
2268
+ duration_api_ms: message.duration_api_ms ?? 0
2269
+ }
2270
+ });
2271
+ } catch {
2272
+ }
2273
+ }
2274
+ yield message;
2275
+ }
2276
+ } catch (error) {
2277
+ if (sessionId) {
2278
+ await Promise.allSettled(pendingToolCalls);
2279
+ try {
2280
+ await client.completeSession({
2281
+ sessionId,
2282
+ success: false,
2283
+ failureReason: error instanceof Error ? error.message : String(error),
2284
+ durationMs: Date.now() - startTime
2285
+ });
2286
+ } catch {
2287
+ }
2288
+ }
2289
+ throw error;
1569
2290
  }
1570
- }).catch((err) => {
1571
- console.warn("Sentrial: Failed to track LLM error:", err.message);
1572
- });
1573
- }
2291
+ })();
2292
+ };
1574
2293
  }
1575
2294
 
1576
2295
  // src/decorators.ts
1577
2296
  var _defaultClient3 = null;
1578
- var _currentInteraction = null;
2297
+ var _currentInteraction = createContextVar(null);
1579
2298
  function getClient3() {
1580
2299
  if (!_defaultClient3) {
1581
2300
  try {
@@ -1595,7 +2314,7 @@ function getCurrentSessionId() {
1595
2314
  return getSessionContext();
1596
2315
  }
1597
2316
  function getCurrentInteraction() {
1598
- return _currentInteraction;
2317
+ return _currentInteraction.get();
1599
2318
  }
1600
2319
  function withTool(name, fn) {
1601
2320
  const isAsync = fn.constructor.name === "AsyncFunction";
@@ -1696,10 +2415,11 @@ function withSession(agentName, fn, options = {}) {
1696
2415
  input: userInput
1697
2416
  });
1698
2417
  const sessionId = interaction.getSessionId();
2418
+ let sessionTokens;
1699
2419
  if (sessionId) {
1700
- setSessionContext(sessionId, client);
2420
+ sessionTokens = _setSessionContextWithTokens(sessionId, client);
1701
2421
  }
1702
- _currentInteraction = interaction;
2422
+ const interactionToken = _currentInteraction.set(interaction);
1703
2423
  try {
1704
2424
  const result = await fn(...args);
1705
2425
  let output;
@@ -1724,8 +2444,10 @@ function withSession(agentName, fn, options = {}) {
1724
2444
  });
1725
2445
  throw error;
1726
2446
  } finally {
1727
- clearSessionContext();
1728
- _currentInteraction = null;
2447
+ if (sessionTokens) {
2448
+ _restoreSessionContext(sessionTokens);
2449
+ }
2450
+ _currentInteraction.reset(interactionToken);
1729
2451
  }
1730
2452
  };
1731
2453
  }
@@ -1811,10 +2533,11 @@ function TrackSession(agentName, options) {
1811
2533
  input: userInput
1812
2534
  });
1813
2535
  const sessionId = interaction.getSessionId();
2536
+ let sessionTokens;
1814
2537
  if (sessionId) {
1815
- setSessionContext(sessionId, client);
2538
+ sessionTokens = _setSessionContextWithTokens(sessionId, client);
1816
2539
  }
1817
- _currentInteraction = interaction;
2540
+ const interactionToken = _currentInteraction.set(interaction);
1818
2541
  try {
1819
2542
  const result = await originalMethod.apply(this, args);
1820
2543
  let output;
@@ -1839,8 +2562,10 @@ function TrackSession(agentName, options) {
1839
2562
  });
1840
2563
  throw error;
1841
2564
  } finally {
1842
- clearSessionContext();
1843
- _currentInteraction = null;
2565
+ if (sessionTokens) {
2566
+ _restoreSessionContext(sessionTokens);
2567
+ }
2568
+ _currentInteraction.reset(interactionToken);
1844
2569
  }
1845
2570
  };
1846
2571
  return descriptor;
@@ -1853,6 +2578,8 @@ var SessionContext = class {
1853
2578
  client;
1854
2579
  interaction = null;
1855
2580
  output;
2581
+ sessionTokens;
2582
+ interactionToken;
1856
2583
  constructor(options) {
1857
2584
  this.userId = options.userId;
1858
2585
  this.agent = options.agent;
@@ -1871,9 +2598,9 @@ var SessionContext = class {
1871
2598
  });
1872
2599
  const sessionId = this.interaction.getSessionId();
1873
2600
  if (sessionId) {
1874
- setSessionContext(sessionId, this.client);
2601
+ this.sessionTokens = _setSessionContextWithTokens(sessionId, this.client);
1875
2602
  }
1876
- _currentInteraction = this.interaction;
2603
+ this.interactionToken = _currentInteraction.set(this.interaction);
1877
2604
  return this;
1878
2605
  }
1879
2606
  /**
@@ -1893,8 +2620,12 @@ var SessionContext = class {
1893
2620
  failureReason: options?.error
1894
2621
  });
1895
2622
  }
1896
- clearSessionContext();
1897
- _currentInteraction = null;
2623
+ if (this.sessionTokens) {
2624
+ _restoreSessionContext(this.sessionTokens);
2625
+ }
2626
+ if (this.interactionToken) {
2627
+ _currentInteraction.reset(this.interactionToken);
2628
+ }
1898
2629
  }
1899
2630
  /**
1900
2631
  * Get the session ID
@@ -1948,30 +2679,31 @@ function serializeOutput(value) {
1948
2679
  }
1949
2680
 
1950
2681
  // src/context.ts
1951
- var _experimentContext = null;
2682
+ var _experimentContext = createContextVar(null);
1952
2683
  function getSystemPrompt(defaultPrompt) {
1953
- if (_experimentContext?.systemPrompt) {
1954
- return _experimentContext.systemPrompt;
2684
+ const ctx = _experimentContext.get();
2685
+ if (ctx?.systemPrompt) {
2686
+ return ctx.systemPrompt;
1955
2687
  }
1956
2688
  return defaultPrompt ?? "";
1957
2689
  }
1958
2690
  function getExperimentContext() {
1959
- return _experimentContext;
2691
+ return _experimentContext.get();
1960
2692
  }
1961
2693
  function isExperimentMode() {
1962
- return _experimentContext !== null;
2694
+ return _experimentContext.get() !== null;
1963
2695
  }
1964
2696
  function getVariantName() {
1965
- return _experimentContext?.variantName ?? null;
2697
+ return _experimentContext.get()?.variantName ?? null;
1966
2698
  }
1967
2699
  function getExperimentId() {
1968
- return _experimentContext?.experimentId ?? null;
2700
+ return _experimentContext.get()?.experimentId ?? null;
1969
2701
  }
1970
2702
  function setExperimentContext(context) {
1971
- _experimentContext = context;
2703
+ _experimentContext.set(context);
1972
2704
  }
1973
2705
  function clearExperimentContext() {
1974
- _experimentContext = null;
2706
+ _experimentContext.set(null);
1975
2707
  }
1976
2708
 
1977
2709
  // src/experiment.ts
@@ -2304,6 +3036,7 @@ var Experiment = class {
2304
3036
  };
2305
3037
  export {
2306
3038
  ApiError,
3039
+ EventBatcher,
2307
3040
  EventType,
2308
3041
  Experiment,
2309
3042
  ExperimentRunTracker,
@@ -2323,6 +3056,7 @@ export {
2323
3056
  clearSessionContext,
2324
3057
  configure,
2325
3058
  configureVercel,
3059
+ createContextVar,
2326
3060
  getCurrentInteraction,
2327
3061
  getCurrentSessionId,
2328
3062
  getExperimentContext,
@@ -2345,6 +3079,7 @@ export {
2345
3079
  withTool,
2346
3080
  wrapAISDK,
2347
3081
  wrapAnthropic,
3082
+ wrapClaudeAgent,
2348
3083
  wrapGoogle,
2349
3084
  wrapLLM,
2350
3085
  wrapOpenAI