@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.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,
@@ -1078,8 +1825,10 @@ function wrapStreamText(originalFn, client, config) {
1078
1825
  tools: params.tools ? wrapToolsAsync(params.tools, sessionPromise, client) : void 0
1079
1826
  };
1080
1827
  const result = originalFn(wrappedParams);
1828
+ const originalTextStream = result.textStream;
1829
+ let fullText = "";
1081
1830
  let tracked = false;
1082
- async function trackCompletion(fullText, error) {
1831
+ async function trackCompletion(text, error) {
1083
1832
  if (tracked) return;
1084
1833
  tracked = true;
1085
1834
  const durationMs = Date.now() - startTime;
@@ -1090,80 +1839,118 @@ function wrapStreamText(originalFn, client, config) {
1090
1839
  sessionId: sid,
1091
1840
  errorType: error.name || "Error",
1092
1841
  errorMessage: error.message || "Unknown error"
1842
+ }).catch(() => {
1093
1843
  });
1094
1844
  await client.completeSession({
1095
1845
  sessionId: sid,
1096
1846
  success: false,
1097
1847
  failureReason: error.message || "Unknown error",
1098
1848
  durationMs
1849
+ }).catch(() => {
1099
1850
  });
1100
1851
  return;
1101
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
+ }
1102
1859
  let usage;
1103
1860
  try {
1104
1861
  usage = result.usage ? await result.usage : void 0;
1105
1862
  } catch {
1106
1863
  }
1107
- const promptTokens = usage?.promptTokens || 0;
1108
- const completionTokens = usage?.completionTokens || 0;
1109
- const totalTokens = usage?.totalTokens || promptTokens + completionTokens;
1110
- const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
1111
- await client.trackEvent({
1112
- sessionId: sid,
1113
- eventType: "llm_call",
1114
- eventData: {
1115
- model: modelId,
1116
- provider,
1117
- prompt_tokens: promptTokens,
1118
- completion_tokens: completionTokens,
1119
- total_tokens: totalTokens
1120
- }
1121
- });
1122
- await client.completeSession({
1123
- sessionId: sid,
1124
- success: true,
1125
- output: fullText,
1126
- durationMs,
1127
- estimatedCost: cost,
1128
- promptTokens,
1129
- completionTokens,
1130
- totalTokens
1131
- });
1132
- }
1133
- const textProp = result.text;
1134
- if (typeof textProp === "string") {
1135
- trackCompletion(textProp).catch(() => {
1136
- });
1137
- } else if (textProp != null && typeof textProp.then === "function") {
1138
- const originalTextPromise = textProp;
1139
- result.text = originalTextPromise.then((text) => {
1140
- trackCompletion(text).catch(() => {
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({
1877
+ sessionId: sid,
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(() => {
1891
+ });
1141
1892
  });
1142
- return text;
1143
- }).catch((err) => {
1144
- trackCompletion("", err instanceof Error ? err : new Error(String(err))).catch(() => {
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(() => {
1145
1908
  });
1146
- throw err;
1147
- });
1148
- } else {
1149
- const originalTextStream = result.textStream;
1150
- let fullText = "";
1151
- result.textStream = (async function* () {
1152
- try {
1153
- for await (const chunk of originalTextStream) {
1154
- fullText += chunk;
1155
- yield chunk;
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
1156
1923
  }
1157
- await trackCompletion(fullText);
1158
- } catch (error) {
1159
- await trackCompletion(
1160
- "",
1161
- error instanceof Error ? error : new Error(String(error))
1162
- );
1163
- throw error;
1164
- }
1165
- })();
1924
+ }).catch(() => {
1925
+ });
1926
+ await client.completeSession({
1927
+ sessionId: sid,
1928
+ success: true,
1929
+ output: text,
1930
+ durationMs,
1931
+ estimatedCost: cost,
1932
+ promptTokens,
1933
+ completionTokens,
1934
+ totalTokens
1935
+ }).catch(() => {
1936
+ });
1937
+ }
1166
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
+ })();
1167
1954
  return result;
1168
1955
  };
1169
1956
  }
@@ -1191,10 +1978,22 @@ function wrapGenerateObject(originalFn, client, config) {
1191
1978
  const result = await originalFn(params);
1192
1979
  const durationMs = Date.now() - startTime;
1193
1980
  const resolvedModelId = result.response?.modelId || modelId;
1194
- const promptTokens = result.usage?.promptTokens || 0;
1195
- const completionTokens = result.usage?.completionTokens || 0;
1196
- const totalTokens = result.usage?.totalTokens || promptTokens + completionTokens;
1981
+ const promptTokens = result.usage?.promptTokens ?? 0;
1982
+ const completionTokens = result.usage?.completionTokens ?? 0;
1983
+ const totalTokens = result.usage?.totalTokens ?? promptTokens + completionTokens;
1197
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
1994
+ }
1995
+ }).catch(() => {
1996
+ });
1198
1997
  await client.completeSession({
1199
1998
  sessionId,
1200
1999
  success: true,
@@ -1223,393 +2022,279 @@ function wrapGenerateObject(originalFn, client, config) {
1223
2022
  }
1224
2023
  };
1225
2024
  }
1226
- function wrapStreamObject(originalFn, client, config) {
1227
- return (params) => {
1228
- const startTime = Date.now();
1229
- const { modelId, provider } = extractModelInfo(params.model);
1230
- const input = extractInput(params);
1231
- const sessionPromise = (async () => {
1232
- try {
1233
- const id = await client.createSession({
1234
- name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1235
- agentName: config.defaultAgent ?? "vercel-ai-sdk",
1236
- userId: config.userId ?? "anonymous",
1237
- convoId: config.convoId,
1238
- metadata: {
1239
- model: modelId,
1240
- provider,
1241
- function: "streamObject"
1242
- }
1243
- });
1244
- if (id) {
1245
- client.setInput(id, input).catch(() => {
1246
- });
1247
- }
1248
- return id;
1249
- } catch {
1250
- return null;
1251
- }
1252
- })();
1253
- const result = originalFn(params);
1254
- if (result.object) {
1255
- const originalObjectPromise = result.object;
1256
- result.object = originalObjectPromise.then(async (obj) => {
1257
- const durationMs = Date.now() - startTime;
1258
- const sid = await sessionPromise;
1259
- if (sid) {
1260
- let usage;
1261
- try {
1262
- usage = result.usage ? await result.usage : void 0;
1263
- } catch {
1264
- }
1265
- const promptTokens = usage?.promptTokens || 0;
1266
- const completionTokens = usage?.completionTokens || 0;
1267
- const totalTokens = usage?.totalTokens || promptTokens + completionTokens;
1268
- const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
1269
- await client.completeSession({
1270
- sessionId: sid,
1271
- success: true,
1272
- output: JSON.stringify(obj),
1273
- durationMs,
1274
- estimatedCost: cost,
1275
- promptTokens,
1276
- completionTokens,
1277
- totalTokens
1278
- });
1279
- }
1280
- return obj;
1281
- }).catch(async (error) => {
1282
- const durationMs = Date.now() - startTime;
1283
- const sid = await sessionPromise;
1284
- if (sid) {
1285
- await client.trackError({
1286
- sessionId: sid,
1287
- errorType: error instanceof Error ? error.name : "Error",
1288
- errorMessage: error instanceof Error ? error.message : "Unknown error"
1289
- });
1290
- await client.completeSession({
1291
- sessionId: sid,
1292
- success: false,
1293
- failureReason: error instanceof Error ? error.message : "Unknown error",
1294
- durationMs
1295
- });
1296
- }
1297
- throw error;
1298
- });
1299
- }
1300
- return result;
1301
- };
1302
- }
1303
- function wrapAISDK(ai, options) {
1304
- const client = options?.client ?? getClient2();
1305
- const config = {
1306
- defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
1307
- userId: options?.userId ?? _globalConfig.userId,
1308
- convoId: options?.convoId ?? _globalConfig.convoId
1309
- };
1310
- return {
1311
- generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
1312
- () => Promise.reject(new Error("generateText not available")),
1313
- client,
1314
- config
1315
- ),
1316
- streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
1317
- })() }), client, config),
1318
- generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
1319
- () => Promise.reject(new Error("generateObject not available")),
1320
- client,
1321
- config
1322
- ),
1323
- streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
1324
- };
1325
- }
1326
-
1327
- // src/wrappers.ts
1328
- var _currentSessionId = null;
1329
- var _currentClient = null;
1330
- var _defaultClient2 = null;
1331
- function setSessionContext(sessionId, client) {
1332
- _currentSessionId = sessionId;
1333
- if (client) {
1334
- _currentClient = client;
1335
- }
1336
- }
1337
- function clearSessionContext() {
1338
- _currentSessionId = null;
1339
- _currentClient = null;
1340
- }
1341
- function getSessionContext() {
1342
- return _currentSessionId;
1343
- }
1344
- function setDefaultClient(client) {
1345
- _defaultClient2 = client;
1346
- }
1347
- function getTrackingClient() {
1348
- return _currentClient ?? _defaultClient2;
1349
- }
1350
- function wrapOpenAI(client, options = {}) {
1351
- const { trackWithoutSession = false } = options;
1352
- const chat = client.chat;
1353
- if (!chat?.completions?.create) {
1354
- console.warn("Sentrial: OpenAI client does not have chat.completions.create");
1355
- return client;
1356
- }
1357
- const originalCreate = chat.completions.create.bind(chat.completions);
1358
- chat.completions.create = async function(...args) {
1359
- const startTime = Date.now();
1360
- const params = args[0] ?? {};
1361
- const messages = params.messages ?? [];
1362
- const model = params.model ?? "unknown";
1363
- try {
1364
- const response = await originalCreate(...args);
1365
- const durationMs = Date.now() - startTime;
1366
- const promptTokens = response.usage?.prompt_tokens ?? 0;
1367
- const completionTokens = response.usage?.completion_tokens ?? 0;
1368
- const totalTokens = response.usage?.total_tokens ?? 0;
1369
- let outputContent = "";
1370
- if (response.choices?.[0]?.message?.content) {
1371
- outputContent = response.choices[0].message.content;
1372
- }
1373
- const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
1374
- trackLLMCall({
1375
- provider: "openai",
1376
- model,
1377
- messages,
1378
- output: outputContent,
1379
- promptTokens,
1380
- completionTokens,
1381
- totalTokens,
1382
- cost,
1383
- durationMs,
1384
- trackWithoutSession
1385
- });
1386
- return response;
1387
- } catch (error) {
1388
- const durationMs = Date.now() - startTime;
1389
- trackLLMError({
1390
- provider: "openai",
1391
- model,
1392
- messages,
1393
- error,
1394
- durationMs,
1395
- trackWithoutSession
1396
- });
1397
- throw error;
1398
- }
1399
- };
1400
- return client;
1401
- }
1402
- function wrapAnthropic(client, options = {}) {
1403
- const { trackWithoutSession = false } = options;
1404
- const messages = client.messages;
1405
- if (!messages?.create) {
1406
- console.warn("Sentrial: Anthropic client does not have messages.create");
1407
- return client;
1408
- }
1409
- const originalCreate = messages.create.bind(messages);
1410
- messages.create = async function(...args) {
1411
- const startTime = Date.now();
1412
- const params = args[0] ?? {};
1413
- const inputMessages = params.messages ?? [];
1414
- const model = params.model ?? "unknown";
1415
- const system = params.system ?? "";
1416
- try {
1417
- const response = await originalCreate(...args);
1418
- const durationMs = Date.now() - startTime;
1419
- const promptTokens = response.usage?.input_tokens ?? 0;
1420
- const completionTokens = response.usage?.output_tokens ?? 0;
1421
- const totalTokens = promptTokens + completionTokens;
1422
- let outputContent = "";
1423
- if (response.content) {
1424
- for (const block of response.content) {
1425
- if (block.type === "text") {
1426
- outputContent += block.text;
1427
- }
1428
- }
1429
- }
1430
- const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
1431
- const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
1432
- trackLLMCall({
1433
- provider: "anthropic",
1434
- model,
1435
- messages: fullMessages,
1436
- output: outputContent,
1437
- promptTokens,
1438
- completionTokens,
1439
- totalTokens,
1440
- cost,
1441
- durationMs,
1442
- trackWithoutSession
1443
- });
1444
- return response;
1445
- } catch (error) {
1446
- const durationMs = Date.now() - startTime;
1447
- trackLLMError({
1448
- provider: "anthropic",
1449
- model,
1450
- messages: inputMessages,
1451
- error,
1452
- durationMs,
1453
- trackWithoutSession
1454
- });
1455
- throw error;
1456
- }
1457
- };
1458
- return client;
1459
- }
1460
- function wrapGoogle(model, options = {}) {
1461
- const { trackWithoutSession = false } = options;
1462
- const originalGenerate = model.generateContent;
1463
- if (!originalGenerate) {
1464
- console.warn("Sentrial: Google model does not have generateContent");
1465
- return model;
1466
- }
1467
- model.generateContent = async function(...args) {
2025
+ function wrapStreamObject(originalFn, client, config) {
2026
+ return (params) => {
1468
2027
  const startTime = Date.now();
1469
- const contents = args[0];
1470
- const modelName = model.model ?? "gemini-unknown";
1471
- const messages = googleContentsToMessages(contents);
1472
- try {
1473
- 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) {
1474
2054
  const durationMs = Date.now() - startTime;
1475
- let promptTokens = 0;
1476
- let completionTokens = 0;
1477
- if (response.usageMetadata) {
1478
- promptTokens = response.usageMetadata.promptTokenCount ?? 0;
1479
- 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;
1480
2072
  }
1481
- const totalTokens = promptTokens + completionTokens;
1482
- let outputContent = "";
2073
+ let usage;
1483
2074
  try {
1484
- outputContent = response.response?.text() ?? "";
2075
+ usage = result.usage ? await result.usage : void 0;
1485
2076
  } catch {
1486
2077
  }
1487
- const cost = calculateGoogleCost({ model: modelName, inputTokens: promptTokens, outputTokens: completionTokens });
1488
- trackLLMCall({
1489
- provider: "google",
1490
- model: modelName,
1491
- messages,
1492
- 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,
1493
2100
  promptTokens,
1494
2101
  completionTokens,
1495
- totalTokens,
1496
- cost,
1497
- durationMs,
1498
- trackWithoutSession
2102
+ totalTokens
2103
+ }).catch(() => {
1499
2104
  });
1500
- return response;
1501
- } catch (error) {
1502
- const durationMs = Date.now() - startTime;
1503
- trackLLMError({
1504
- provider: "google",
1505
- model: modelName,
1506
- messages,
1507
- error,
1508
- durationMs,
1509
- 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)));
1510
2120
  });
1511
- throw error;
1512
2121
  }
2122
+ return result;
1513
2123
  };
1514
- return model;
1515
- }
1516
- function googleContentsToMessages(contents) {
1517
- if (typeof contents === "string") {
1518
- return [{ role: "user", content: contents }];
1519
- }
1520
- if (Array.isArray(contents)) {
1521
- return contents.map((item) => {
1522
- if (typeof item === "string") {
1523
- return { role: "user", content: item };
1524
- }
1525
- if (item && typeof item === "object") {
1526
- return { role: item.role ?? "user", content: String(item.content ?? item) };
1527
- }
1528
- return { role: "user", content: String(item) };
1529
- });
1530
- }
1531
- return [{ role: "user", content: String(contents) }];
1532
2124
  }
1533
- function wrapLLM(client, provider) {
1534
- if (provider === "openai" || client.chat?.completions?.create) {
1535
- return wrapOpenAI(client);
1536
- }
1537
- if (provider === "anthropic" || client.messages?.create) {
1538
- return wrapAnthropic(client);
1539
- }
1540
- if (provider === "google" || client.generateContent) {
1541
- return wrapGoogle(client);
1542
- }
1543
- console.warn("Sentrial: Unknown LLM client type. No auto-tracking applied.");
1544
- 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
+ };
1545
2147
  }
1546
- function trackLLMCall(params) {
1547
- const client = getTrackingClient();
1548
- if (!client) return;
1549
- const sessionId = _currentSessionId;
1550
- if (!sessionId && !params.trackWithoutSession) {
1551
- return;
1552
- }
1553
- if (sessionId) {
1554
- client.trackToolCall({
1555
- sessionId,
1556
- toolName: `llm:${params.provider}:${params.model}`,
1557
- toolInput: {
1558
- messages: params.messages,
1559
- model: params.model,
1560
- provider: params.provider
1561
- },
1562
- toolOutput: {
1563
- content: params.output,
1564
- tokens: {
1565
- prompt: params.promptTokens,
1566
- completion: params.completionTokens,
1567
- total: params.totalTokens
1568
- },
1569
- cost_usd: params.cost
1570
- },
1571
- reasoning: `LLM call to ${params.provider} ${params.model}`,
1572
- estimatedCost: params.cost,
1573
- tokenCount: params.totalTokens,
1574
- metadata: {
1575
- provider: params.provider,
1576
- model: params.model,
1577
- duration_ms: params.durationMs,
1578
- prompt_tokens: params.promptTokens,
1579
- completion_tokens: params.completionTokens
1580
- }
1581
- }).catch((err) => {
1582
- 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;
1583
2165
  });
1584
- }
1585
- }
1586
- function trackLLMError(params) {
1587
- const client = getTrackingClient();
1588
- if (!client) return;
1589
- const sessionId = _currentSessionId;
1590
- if (!sessionId && !params.trackWithoutSession) {
1591
- return;
1592
- }
1593
- if (sessionId) {
1594
- client.trackError({
1595
- sessionId,
1596
- errorMessage: params.error.message,
1597
- errorType: params.error.name,
1598
- toolName: `llm:${params.provider}:${params.model}`,
1599
- metadata: {
1600
- provider: params.provider,
1601
- model: params.model,
1602
- 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;
1603
2290
  }
1604
- }).catch((err) => {
1605
- console.warn("Sentrial: Failed to track LLM error:", err.message);
1606
- });
1607
- }
2291
+ })();
2292
+ };
1608
2293
  }
1609
2294
 
1610
2295
  // src/decorators.ts
1611
2296
  var _defaultClient3 = null;
1612
- var _currentInteraction = null;
2297
+ var _currentInteraction = createContextVar(null);
1613
2298
  function getClient3() {
1614
2299
  if (!_defaultClient3) {
1615
2300
  try {
@@ -1629,7 +2314,7 @@ function getCurrentSessionId() {
1629
2314
  return getSessionContext();
1630
2315
  }
1631
2316
  function getCurrentInteraction() {
1632
- return _currentInteraction;
2317
+ return _currentInteraction.get();
1633
2318
  }
1634
2319
  function withTool(name, fn) {
1635
2320
  const isAsync = fn.constructor.name === "AsyncFunction";
@@ -1730,10 +2415,11 @@ function withSession(agentName, fn, options = {}) {
1730
2415
  input: userInput
1731
2416
  });
1732
2417
  const sessionId = interaction.getSessionId();
2418
+ let sessionTokens;
1733
2419
  if (sessionId) {
1734
- setSessionContext(sessionId, client);
2420
+ sessionTokens = _setSessionContextWithTokens(sessionId, client);
1735
2421
  }
1736
- _currentInteraction = interaction;
2422
+ const interactionToken = _currentInteraction.set(interaction);
1737
2423
  try {
1738
2424
  const result = await fn(...args);
1739
2425
  let output;
@@ -1758,8 +2444,10 @@ function withSession(agentName, fn, options = {}) {
1758
2444
  });
1759
2445
  throw error;
1760
2446
  } finally {
1761
- clearSessionContext();
1762
- _currentInteraction = null;
2447
+ if (sessionTokens) {
2448
+ _restoreSessionContext(sessionTokens);
2449
+ }
2450
+ _currentInteraction.reset(interactionToken);
1763
2451
  }
1764
2452
  };
1765
2453
  }
@@ -1845,10 +2533,11 @@ function TrackSession(agentName, options) {
1845
2533
  input: userInput
1846
2534
  });
1847
2535
  const sessionId = interaction.getSessionId();
2536
+ let sessionTokens;
1848
2537
  if (sessionId) {
1849
- setSessionContext(sessionId, client);
2538
+ sessionTokens = _setSessionContextWithTokens(sessionId, client);
1850
2539
  }
1851
- _currentInteraction = interaction;
2540
+ const interactionToken = _currentInteraction.set(interaction);
1852
2541
  try {
1853
2542
  const result = await originalMethod.apply(this, args);
1854
2543
  let output;
@@ -1873,8 +2562,10 @@ function TrackSession(agentName, options) {
1873
2562
  });
1874
2563
  throw error;
1875
2564
  } finally {
1876
- clearSessionContext();
1877
- _currentInteraction = null;
2565
+ if (sessionTokens) {
2566
+ _restoreSessionContext(sessionTokens);
2567
+ }
2568
+ _currentInteraction.reset(interactionToken);
1878
2569
  }
1879
2570
  };
1880
2571
  return descriptor;
@@ -1887,6 +2578,8 @@ var SessionContext = class {
1887
2578
  client;
1888
2579
  interaction = null;
1889
2580
  output;
2581
+ sessionTokens;
2582
+ interactionToken;
1890
2583
  constructor(options) {
1891
2584
  this.userId = options.userId;
1892
2585
  this.agent = options.agent;
@@ -1905,9 +2598,9 @@ var SessionContext = class {
1905
2598
  });
1906
2599
  const sessionId = this.interaction.getSessionId();
1907
2600
  if (sessionId) {
1908
- setSessionContext(sessionId, this.client);
2601
+ this.sessionTokens = _setSessionContextWithTokens(sessionId, this.client);
1909
2602
  }
1910
- _currentInteraction = this.interaction;
2603
+ this.interactionToken = _currentInteraction.set(this.interaction);
1911
2604
  return this;
1912
2605
  }
1913
2606
  /**
@@ -1927,8 +2620,12 @@ var SessionContext = class {
1927
2620
  failureReason: options?.error
1928
2621
  });
1929
2622
  }
1930
- clearSessionContext();
1931
- _currentInteraction = null;
2623
+ if (this.sessionTokens) {
2624
+ _restoreSessionContext(this.sessionTokens);
2625
+ }
2626
+ if (this.interactionToken) {
2627
+ _currentInteraction.reset(this.interactionToken);
2628
+ }
1932
2629
  }
1933
2630
  /**
1934
2631
  * Get the session ID
@@ -1982,30 +2679,31 @@ function serializeOutput(value) {
1982
2679
  }
1983
2680
 
1984
2681
  // src/context.ts
1985
- var _experimentContext = null;
2682
+ var _experimentContext = createContextVar(null);
1986
2683
  function getSystemPrompt(defaultPrompt) {
1987
- if (_experimentContext?.systemPrompt) {
1988
- return _experimentContext.systemPrompt;
2684
+ const ctx = _experimentContext.get();
2685
+ if (ctx?.systemPrompt) {
2686
+ return ctx.systemPrompt;
1989
2687
  }
1990
2688
  return defaultPrompt ?? "";
1991
2689
  }
1992
2690
  function getExperimentContext() {
1993
- return _experimentContext;
2691
+ return _experimentContext.get();
1994
2692
  }
1995
2693
  function isExperimentMode() {
1996
- return _experimentContext !== null;
2694
+ return _experimentContext.get() !== null;
1997
2695
  }
1998
2696
  function getVariantName() {
1999
- return _experimentContext?.variantName ?? null;
2697
+ return _experimentContext.get()?.variantName ?? null;
2000
2698
  }
2001
2699
  function getExperimentId() {
2002
- return _experimentContext?.experimentId ?? null;
2700
+ return _experimentContext.get()?.experimentId ?? null;
2003
2701
  }
2004
2702
  function setExperimentContext(context) {
2005
- _experimentContext = context;
2703
+ _experimentContext.set(context);
2006
2704
  }
2007
2705
  function clearExperimentContext() {
2008
- _experimentContext = null;
2706
+ _experimentContext.set(null);
2009
2707
  }
2010
2708
 
2011
2709
  // src/experiment.ts
@@ -2338,6 +3036,7 @@ var Experiment = class {
2338
3036
  };
2339
3037
  export {
2340
3038
  ApiError,
3039
+ EventBatcher,
2341
3040
  EventType,
2342
3041
  Experiment,
2343
3042
  ExperimentRunTracker,
@@ -2357,6 +3056,7 @@ export {
2357
3056
  clearSessionContext,
2358
3057
  configure,
2359
3058
  configureVercel,
3059
+ createContextVar,
2360
3060
  getCurrentInteraction,
2361
3061
  getCurrentSessionId,
2362
3062
  getExperimentContext,
@@ -2379,6 +3079,7 @@ export {
2379
3079
  withTool,
2380
3080
  wrapAISDK,
2381
3081
  wrapAnthropic,
3082
+ wrapClaudeAgent,
2382
3083
  wrapGoogle,
2383
3084
  wrapLLM,
2384
3085
  wrapOpenAI