@sentrial/sdk 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1345 -641
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +343 -142
- package/dist/index.d.ts +343 -142
- package/dist/index.js +1341 -640
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -68,7 +68,22 @@ var ValidationError = class extends SentrialError {
|
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
// src/redact.ts
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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/
|
|
240
|
-
var
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
|
1540
|
+
var _defaultClient2 = null;
|
|
795
1541
|
var _globalConfig = {};
|
|
796
1542
|
function configureVercel(config) {
|
|
797
|
-
|
|
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 (!
|
|
810
|
-
|
|
1555
|
+
if (!_defaultClient2) {
|
|
1556
|
+
_defaultClient2 = new SentrialClient();
|
|
811
1557
|
}
|
|
812
|
-
return
|
|
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.
|
|
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
|
|
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
|
|
981
|
-
const completionTokens = result.usage?.completionTokens
|
|
982
|
-
const totalTokens = result.usage?.totalTokens
|
|
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
|
-
|
|
987
|
-
|
|
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
|
|
997
|
-
completion_tokens: step.usage?.completionTokens
|
|
998
|
-
total_tokens: step.usage?.totalTokens
|
|
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(
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1195
|
-
const completionTokens = result.usage?.completionTokens
|
|
1196
|
-
const totalTokens = result.usage?.totalTokens
|
|
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
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1476
|
-
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
-
|
|
1482
|
-
let outputContent = "";
|
|
2073
|
+
let usage;
|
|
1483
2074
|
try {
|
|
1484
|
-
|
|
2075
|
+
usage = result.usage ? await result.usage : void 0;
|
|
1485
2076
|
} catch {
|
|
1486
2077
|
}
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1497
|
-
durationMs,
|
|
1498
|
-
trackWithoutSession
|
|
2102
|
+
totalTokens
|
|
2103
|
+
}).catch(() => {
|
|
1499
2104
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
error
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
})
|
|
1605
|
-
|
|
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
|
-
|
|
2420
|
+
sessionTokens = _setSessionContextWithTokens(sessionId, client);
|
|
1735
2421
|
}
|
|
1736
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
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
|
-
|
|
2538
|
+
sessionTokens = _setSessionContextWithTokens(sessionId, client);
|
|
1850
2539
|
}
|
|
1851
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
2601
|
+
this.sessionTokens = _setSessionContextWithTokens(sessionId, this.client);
|
|
1909
2602
|
}
|
|
1910
|
-
|
|
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
|
-
|
|
1931
|
-
|
|
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
|
-
|
|
1988
|
-
|
|
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
|
|
2703
|
+
_experimentContext.set(context);
|
|
2006
2704
|
}
|
|
2007
2705
|
function clearExperimentContext() {
|
|
2008
|
-
_experimentContext
|
|
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
|