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