@farzanhossans/agentlens 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1103 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var agentlensCore = require('@farzanhossans/agentlens-core');
5
+
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+
13
+ // src/registry.ts
14
+ var LLM_REGISTRY = {
15
+ "api.openai.com": {
16
+ provider: "openai",
17
+ parser: "openai",
18
+ paths: ["/v1/chat/completions", "/v1/completions", "/v1/embeddings"]
19
+ },
20
+ "api.anthropic.com": {
21
+ provider: "anthropic",
22
+ parser: "anthropic",
23
+ paths: ["/v1/messages"]
24
+ },
25
+ "generativelanguage.googleapis.com": {
26
+ provider: "gemini",
27
+ parser: "gemini",
28
+ paths: ["/v1beta/models", "/v1/models"]
29
+ },
30
+ "api.cohere.com": {
31
+ provider: "cohere",
32
+ parser: "cohere",
33
+ paths: ["/v1/chat", "/v1/generate", "/v2/chat"]
34
+ },
35
+ "api.mistral.ai": {
36
+ provider: "mistral",
37
+ parser: "mistral",
38
+ paths: ["/v1/chat/completions"]
39
+ }
40
+ };
41
+ function matchLLM(url) {
42
+ try {
43
+ const parsed = new URL(url);
44
+ const entry = LLM_REGISTRY[parsed.hostname];
45
+ if (!entry) return null;
46
+ const pathMatch = entry.paths.some((p) => parsed.pathname.startsWith(p));
47
+ return pathMatch ? entry : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function matchHostPath(host, path) {
53
+ const entry = LLM_REGISTRY[host];
54
+ if (!entry) return null;
55
+ const pathMatch = entry.paths.some((p) => path.startsWith(p));
56
+ return pathMatch ? entry : null;
57
+ }
58
+
59
+ // src/streaming/sse.ts
60
+ async function captureSSEStream(stream, ctx) {
61
+ const reader = stream.getReader();
62
+ const decoder = new TextDecoder();
63
+ let raw = "";
64
+ try {
65
+ let done = false;
66
+ while (!done) {
67
+ const chunk = await reader.read();
68
+ done = chunk.done;
69
+ if (chunk.value) raw += decoder.decode(chunk.value, { stream: true });
70
+ }
71
+ raw += decoder.decode();
72
+ } catch {
73
+ return;
74
+ }
75
+ const parsed = parseStream(raw, ctx.llm.parser);
76
+ const response = synthesizeResponse(parsed, ctx.llm.parser, ctx.requestBody);
77
+ ctx.transport.push({
78
+ provider: ctx.llm.provider,
79
+ parser: ctx.llm.parser,
80
+ request: ctx.requestBody,
81
+ response,
82
+ latency: ctx.latency,
83
+ status: 200,
84
+ isStream: true
85
+ });
86
+ }
87
+ function parseStream(raw, parser) {
88
+ switch (parser) {
89
+ case "anthropic":
90
+ return parseAnthropicSSE(raw);
91
+ case "gemini":
92
+ return parseGeminiSSE(raw);
93
+ case "cohere":
94
+ return parseCohereSSE(raw);
95
+ case "openai":
96
+ case "mistral":
97
+ return parseOpenAISSE(raw);
98
+ }
99
+ }
100
+ function parseOpenAISSE(raw) {
101
+ let outputText = "";
102
+ let inputTokens = 0;
103
+ let outputTokens = 0;
104
+ for (const line of raw.split("\n")) {
105
+ if (!line.startsWith("data: ")) continue;
106
+ const data = line.slice(6).trim();
107
+ if (data === "" || data === "[DONE]") continue;
108
+ try {
109
+ const evt = JSON.parse(data);
110
+ const choice = evt.choices?.[0];
111
+ if (choice?.delta?.content) outputText += choice.delta.content;
112
+ else if (typeof choice?.text === "string") outputText += choice.text;
113
+ if (evt.usage) {
114
+ if (typeof evt.usage.prompt_tokens === "number") inputTokens = evt.usage.prompt_tokens;
115
+ if (typeof evt.usage.completion_tokens === "number") outputTokens = evt.usage.completion_tokens;
116
+ }
117
+ } catch {
118
+ continue;
119
+ }
120
+ }
121
+ return { outputText, inputTokens, outputTokens };
122
+ }
123
+ function parseAnthropicSSE(raw) {
124
+ let outputText = "";
125
+ let inputTokens = 0;
126
+ let outputTokens = 0;
127
+ for (const line of raw.split("\n")) {
128
+ if (!line.startsWith("data: ")) continue;
129
+ const data = line.slice(6).trim();
130
+ if (data === "" || data === "[DONE]") continue;
131
+ try {
132
+ const evt = JSON.parse(data);
133
+ if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta" && evt.delta.text) {
134
+ outputText += evt.delta.text;
135
+ }
136
+ const u = evt.message?.usage ?? evt.usage;
137
+ if (u) {
138
+ if (typeof u.input_tokens === "number") inputTokens = u.input_tokens;
139
+ if (typeof u.output_tokens === "number") outputTokens = u.output_tokens;
140
+ }
141
+ } catch {
142
+ continue;
143
+ }
144
+ }
145
+ return { outputText, inputTokens, outputTokens };
146
+ }
147
+ function parseGeminiSSE(raw) {
148
+ let outputText = "";
149
+ let inputTokens = 0;
150
+ let outputTokens = 0;
151
+ const candidates = extractGeminiJsonChunks(raw);
152
+ for (const evt of candidates) {
153
+ const evtCandidates = evt.candidates;
154
+ if (Array.isArray(evtCandidates)) {
155
+ for (const c of evtCandidates) {
156
+ const parts = c.content?.parts;
157
+ if (Array.isArray(parts)) {
158
+ for (const p of parts) {
159
+ if (typeof p.text === "string") outputText += p.text;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ const usage = evt.usageMetadata;
165
+ if (usage) {
166
+ if (typeof usage.promptTokenCount === "number") inputTokens = usage.promptTokenCount;
167
+ if (typeof usage.candidatesTokenCount === "number") outputTokens = usage.candidatesTokenCount;
168
+ }
169
+ }
170
+ return { outputText, inputTokens, outputTokens };
171
+ }
172
+ function extractGeminiJsonChunks(raw) {
173
+ const out = [];
174
+ for (const line of raw.split("\n")) {
175
+ if (!line.startsWith("data: ")) continue;
176
+ const body = line.slice(6).trim();
177
+ if (!body || body === "[DONE]") continue;
178
+ try {
179
+ out.push(JSON.parse(body));
180
+ } catch {
181
+ continue;
182
+ }
183
+ }
184
+ if (out.length > 0) return out;
185
+ try {
186
+ const parsed = JSON.parse(raw.trim());
187
+ if (Array.isArray(parsed)) return parsed;
188
+ if (parsed && typeof parsed === "object") return [parsed];
189
+ } catch {
190
+ }
191
+ return out;
192
+ }
193
+ function parseCohereSSE(raw) {
194
+ let outputText = "";
195
+ let inputTokens = 0;
196
+ let outputTokens = 0;
197
+ for (const line of raw.split("\n")) {
198
+ const trimmed = line.trim();
199
+ if (!trimmed) continue;
200
+ const body = trimmed.startsWith("data: ") ? trimmed.slice(6).trim() : trimmed;
201
+ if (!body || body === "[DONE]" || body.startsWith("event:")) continue;
202
+ let evt;
203
+ try {
204
+ evt = JSON.parse(body);
205
+ } catch {
206
+ continue;
207
+ }
208
+ if (evt.event_type === "text-generation" && typeof evt.text === "string") {
209
+ outputText += evt.text;
210
+ }
211
+ if (evt.event_type === "stream-end") {
212
+ const resp = evt.response;
213
+ const billed = resp?.meta?.billed_units;
214
+ if (billed) {
215
+ if (typeof billed.input_tokens === "number") inputTokens = billed.input_tokens;
216
+ if (typeof billed.output_tokens === "number") outputTokens = billed.output_tokens;
217
+ }
218
+ }
219
+ const delta = evt.delta;
220
+ const text = delta?.message?.content?.text;
221
+ if (typeof text === "string") outputText += text;
222
+ const tokens = delta?.usage?.tokens;
223
+ if (tokens) {
224
+ if (typeof tokens.input_tokens === "number") inputTokens = tokens.input_tokens;
225
+ if (typeof tokens.output_tokens === "number") outputTokens = tokens.output_tokens;
226
+ }
227
+ }
228
+ return { outputText, inputTokens, outputTokens };
229
+ }
230
+ function synthesizeResponse(parsed, parser, request) {
231
+ const model = request?.model ?? "unknown";
232
+ switch (parser) {
233
+ case "anthropic":
234
+ return {
235
+ model,
236
+ content: [{ type: "text", text: parsed.outputText }],
237
+ usage: {
238
+ input_tokens: parsed.inputTokens,
239
+ output_tokens: parsed.outputTokens
240
+ }
241
+ };
242
+ case "openai":
243
+ case "mistral":
244
+ return {
245
+ model,
246
+ choices: [{ message: { content: parsed.outputText, role: "assistant" } }],
247
+ usage: {
248
+ prompt_tokens: parsed.inputTokens,
249
+ completion_tokens: parsed.outputTokens,
250
+ total_tokens: parsed.inputTokens + parsed.outputTokens
251
+ }
252
+ };
253
+ case "gemini":
254
+ return {
255
+ modelVersion: model,
256
+ candidates: [{ content: { parts: [{ text: parsed.outputText }] } }],
257
+ usageMetadata: {
258
+ promptTokenCount: parsed.inputTokens,
259
+ candidatesTokenCount: parsed.outputTokens,
260
+ totalTokenCount: parsed.inputTokens + parsed.outputTokens
261
+ }
262
+ };
263
+ case "cohere":
264
+ return {
265
+ text: parsed.outputText,
266
+ meta: {
267
+ billed_units: {
268
+ input_tokens: parsed.inputTokens,
269
+ output_tokens: parsed.outputTokens
270
+ }
271
+ }
272
+ };
273
+ }
274
+ }
275
+
276
+ // src/interceptors/fetch.ts
277
+ function patchFetch(transport2) {
278
+ const original = globalThis.fetch;
279
+ if (!original) return;
280
+ if (original.__agentlens_patched) return;
281
+ globalThis.__agentlens_originalFetch = original;
282
+ const patched = async (input, init) => {
283
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
284
+ const llm = matchLLM(url);
285
+ if (!llm) return original(input, init);
286
+ const start = Date.now();
287
+ let requestBody = null;
288
+ const rawBody = init?.body ?? null;
289
+ if (typeof rawBody === "string") {
290
+ try {
291
+ requestBody = JSON.parse(rawBody);
292
+ } catch {
293
+ requestBody = null;
294
+ }
295
+ } else if (rawBody && rawBody instanceof ArrayBuffer) {
296
+ try {
297
+ const text = new TextDecoder().decode(rawBody);
298
+ requestBody = JSON.parse(text);
299
+ } catch {
300
+ requestBody = null;
301
+ }
302
+ }
303
+ const isStream = requestBody?.stream === true;
304
+ try {
305
+ const response = await original(input, init);
306
+ const latency = Date.now() - start;
307
+ if (isStream && response.body) {
308
+ const [userStream, ourStream] = response.body.tee();
309
+ void captureSSEStream(ourStream, {
310
+ llm,
311
+ requestBody,
312
+ latency,
313
+ transport: transport2
314
+ });
315
+ return new Response(userStream, {
316
+ status: response.status,
317
+ statusText: response.statusText,
318
+ headers: response.headers
319
+ });
320
+ }
321
+ const clone = response.clone();
322
+ const responseBody = await clone.json().catch(() => null);
323
+ if (responseBody) {
324
+ transport2.push({
325
+ provider: llm.provider,
326
+ parser: llm.parser,
327
+ request: requestBody,
328
+ response: responseBody,
329
+ latency,
330
+ status: response.status,
331
+ isStream: false
332
+ });
333
+ }
334
+ return response;
335
+ } catch (err) {
336
+ transport2.pushError({
337
+ provider: llm.provider,
338
+ request: requestBody,
339
+ error: err instanceof Error ? err.message : String(err),
340
+ latency: Date.now() - start
341
+ });
342
+ throw err;
343
+ }
344
+ };
345
+ patched.__agentlens_patched = true;
346
+ globalThis.fetch = patched;
347
+ }
348
+
349
+ // src/interceptors/https.ts
350
+ function patchHttps(transport2) {
351
+ if (typeof process === "undefined" || typeof __require === "undefined") {
352
+ return;
353
+ }
354
+ try {
355
+ patchModule("https", transport2);
356
+ patchModule("http", transport2);
357
+ } catch {
358
+ }
359
+ }
360
+ function patchModule(name, transport2) {
361
+ let mod;
362
+ try {
363
+ mod = __require(name);
364
+ } catch {
365
+ return;
366
+ }
367
+ if (mod.__agentlens_patched) return;
368
+ const original = mod.request.bind(mod);
369
+ mod.request = function patchedRequest(...args) {
370
+ const ctx = extractContext(args);
371
+ if (!ctx) return original(...args);
372
+ const start = Date.now();
373
+ const requestChunks = [];
374
+ const responseChunks = [];
375
+ const req = original(...args);
376
+ const originalWrite = req.write.bind(req);
377
+ req.write = function(chunk, ...rest) {
378
+ try {
379
+ if (chunk) requestChunks.push(toBuffer(chunk));
380
+ } catch {
381
+ }
382
+ return originalWrite(chunk, ...rest);
383
+ };
384
+ req.on("response", (...resArgs) => {
385
+ const res = resArgs[0];
386
+ res.on("data", (chunk) => {
387
+ try {
388
+ responseChunks.push(toBuffer(chunk));
389
+ } catch {
390
+ }
391
+ });
392
+ res.on("end", () => {
393
+ const latency = Date.now() - start;
394
+ const requestText = Buffer.concat(requestChunks).toString("utf8");
395
+ const responseText = Buffer.concat(responseChunks).toString("utf8");
396
+ const requestBody = safeParse(requestText);
397
+ const responseBody = safeParse(responseText);
398
+ if (responseBody) {
399
+ transport2.push({
400
+ provider: ctx.llm.provider,
401
+ parser: ctx.llm.parser,
402
+ request: requestBody,
403
+ response: responseBody,
404
+ latency,
405
+ status: res.statusCode ?? 0,
406
+ isStream: requestBody?.stream === true
407
+ });
408
+ }
409
+ });
410
+ });
411
+ req.on("error", (...errArgs) => {
412
+ const err = errArgs[0];
413
+ const requestText = Buffer.concat(requestChunks).toString("utf8");
414
+ transport2.pushError({
415
+ provider: ctx.llm.provider,
416
+ request: safeParse(requestText),
417
+ error: err?.message ?? String(err),
418
+ latency: Date.now() - start
419
+ });
420
+ });
421
+ return req;
422
+ };
423
+ mod.__agentlens_patched = true;
424
+ }
425
+ function extractContext(args) {
426
+ let host;
427
+ let path;
428
+ const first = args[0];
429
+ if (typeof first === "string") {
430
+ try {
431
+ const u = new URL(first);
432
+ host = u.hostname;
433
+ path = u.pathname + u.search;
434
+ } catch {
435
+ return null;
436
+ }
437
+ } else if (first instanceof URL) {
438
+ host = first.hostname;
439
+ path = first.pathname + first.search;
440
+ } else if (first && typeof first === "object") {
441
+ const opts = first;
442
+ host = opts.hostname ?? opts.host;
443
+ path = opts.path ?? "/";
444
+ }
445
+ if (args.length > 1 && typeof args[1] === "object" && args[1] !== null && !(args[1] instanceof URL)) {
446
+ const opts = args[1];
447
+ if (!host) host = opts.hostname ?? opts.host;
448
+ if (!path || path === "/") path = opts.path ?? path;
449
+ }
450
+ if (!host || !path) return null;
451
+ const pathOnly = path.split("?")[0];
452
+ const llm = matchHostPath(host, pathOnly);
453
+ if (!llm) return null;
454
+ return { llm };
455
+ }
456
+ function toBuffer(chunk) {
457
+ if (Buffer.isBuffer(chunk)) return chunk;
458
+ if (typeof chunk === "string") return Buffer.from(chunk);
459
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk);
460
+ return Buffer.from(String(chunk));
461
+ }
462
+ function safeParse(text) {
463
+ if (!text) return null;
464
+ try {
465
+ return JSON.parse(text);
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+
471
+ // src/parsers/anthropic.ts
472
+ var ANTHROPIC_COSTS = {
473
+ "claude-opus-4-6": { input: 15e-6, output: 75e-6 },
474
+ "claude-sonnet-4-6": { input: 3e-6, output: 15e-6 },
475
+ "claude-haiku-4-5": { input: 25e-8, output: 125e-8 },
476
+ "claude-3-opus": { input: 15e-6, output: 75e-6 },
477
+ "claude-3-sonnet": { input: 3e-6, output: 15e-6 },
478
+ "claude-3-haiku": { input: 25e-8, output: 125e-8 },
479
+ "claude-3-5-sonnet": { input: 3e-6, output: 15e-6 },
480
+ "claude-3-5-haiku": { input: 1e-6, output: 5e-6 }
481
+ };
482
+ function lookupCost(model) {
483
+ if (ANTHROPIC_COSTS[model]) return ANTHROPIC_COSTS[model];
484
+ for (const key of Object.keys(ANTHROPIC_COSTS)) {
485
+ if (model.startsWith(key)) return ANTHROPIC_COSTS[key];
486
+ }
487
+ return { input: 0, output: 0 };
488
+ }
489
+ function extractInputText(request) {
490
+ if (!request) return "";
491
+ const parts = [];
492
+ if (typeof request.system === "string") parts.push(request.system);
493
+ const messages = request.messages;
494
+ if (Array.isArray(messages)) {
495
+ for (const m of messages) {
496
+ const content = m.content;
497
+ if (typeof content === "string") parts.push(content);
498
+ else if (Array.isArray(content)) {
499
+ for (const block of content) {
500
+ const text = block.text;
501
+ if (typeof text === "string") parts.push(text);
502
+ }
503
+ }
504
+ }
505
+ }
506
+ return parts.join("\n");
507
+ }
508
+ function extractOutputText(response) {
509
+ if (!response) return "";
510
+ const content = response.content;
511
+ if (Array.isArray(content)) {
512
+ return content.map((block) => {
513
+ const b = block;
514
+ return b.type === "text" && typeof b.text === "string" ? b.text : "";
515
+ }).join("");
516
+ }
517
+ return "";
518
+ }
519
+ function parseAnthropic(args) {
520
+ const { request, response, isStream } = args;
521
+ const model = response?.model ?? request?.model ?? "unknown";
522
+ const usage = response?.usage ?? {};
523
+ const inputTokens = usage.input_tokens ?? 0;
524
+ const outputTokens = usage.output_tokens ?? 0;
525
+ const totalTokens = inputTokens + outputTokens;
526
+ const cost = lookupCost(model);
527
+ return {
528
+ model,
529
+ provider: "anthropic",
530
+ inputTokens,
531
+ outputTokens,
532
+ totalTokens,
533
+ costUsd: inputTokens * cost.input + outputTokens * cost.output,
534
+ inputText: extractInputText(request),
535
+ outputText: extractOutputText(response),
536
+ isStream
537
+ };
538
+ }
539
+
540
+ // src/parsers/cohere.ts
541
+ var COHERE_COSTS = {
542
+ "command-r-plus": { input: 3e-6, output: 15e-6 },
543
+ "command-r": { input: 5e-7, output: 15e-7 },
544
+ "command": { input: 1e-6, output: 2e-6 },
545
+ "command-light": { input: 3e-7, output: 6e-7 }
546
+ };
547
+ function lookupCost2(model) {
548
+ if (COHERE_COSTS[model]) return COHERE_COSTS[model];
549
+ for (const key of Object.keys(COHERE_COSTS)) {
550
+ if (model.startsWith(key)) return COHERE_COSTS[key];
551
+ }
552
+ return { input: 0, output: 0 };
553
+ }
554
+ function extractInputText2(request) {
555
+ if (!request) return "";
556
+ if (typeof request.message === "string") return request.message;
557
+ if (typeof request.prompt === "string") return request.prompt;
558
+ const messages = request.messages;
559
+ if (Array.isArray(messages)) {
560
+ return messages.map((m) => {
561
+ const content = m.content;
562
+ if (typeof content === "string") return content;
563
+ return "";
564
+ }).join("\n");
565
+ }
566
+ return "";
567
+ }
568
+ function extractOutputText2(response) {
569
+ if (!response) return "";
570
+ if (typeof response.text === "string") return response.text;
571
+ const message = response.message;
572
+ if (message?.content && Array.isArray(message.content)) {
573
+ return message.content.map((c) => typeof c.text === "string" ? c.text : "").join("");
574
+ }
575
+ const generations = response.generations;
576
+ if (Array.isArray(generations) && generations.length > 0) {
577
+ const first = generations[0];
578
+ if (typeof first.text === "string") return first.text;
579
+ }
580
+ return "";
581
+ }
582
+ function parseCohere(args) {
583
+ const { request, response, isStream } = args;
584
+ const model = request?.model ?? "unknown";
585
+ const meta = response?.meta ?? {};
586
+ const usage = response?.usage ?? {};
587
+ const tokens = meta.billed_units ?? usage.tokens ?? {};
588
+ const inputTokens = tokens.input_tokens ?? 0;
589
+ const outputTokens = tokens.output_tokens ?? 0;
590
+ const cost = lookupCost2(model);
591
+ return {
592
+ model,
593
+ provider: "cohere",
594
+ inputTokens,
595
+ outputTokens,
596
+ totalTokens: inputTokens + outputTokens,
597
+ costUsd: inputTokens * cost.input + outputTokens * cost.output,
598
+ inputText: extractInputText2(request),
599
+ outputText: extractOutputText2(response),
600
+ isStream
601
+ };
602
+ }
603
+
604
+ // src/parsers/gemini.ts
605
+ var GEMINI_COSTS = {
606
+ "gemini-1.5-pro": { input: 125e-8, output: 5e-6 },
607
+ "gemini-1.5-flash": { input: 75e-9, output: 3e-7 },
608
+ "gemini-1.0-pro": { input: 5e-7, output: 15e-7 },
609
+ "gemini-pro": { input: 5e-7, output: 15e-7 }
610
+ };
611
+ function lookupCost3(model) {
612
+ if (GEMINI_COSTS[model]) return GEMINI_COSTS[model];
613
+ for (const key of Object.keys(GEMINI_COSTS)) {
614
+ if (model.startsWith(key)) return GEMINI_COSTS[key];
615
+ }
616
+ return { input: 0, output: 0 };
617
+ }
618
+ function extractInputText3(request) {
619
+ if (!request) return "";
620
+ const contents = request.contents;
621
+ if (!Array.isArray(contents)) return "";
622
+ const parts = [];
623
+ for (const c of contents) {
624
+ const innerParts = c.parts;
625
+ if (Array.isArray(innerParts)) {
626
+ for (const p of innerParts) {
627
+ const text = p.text;
628
+ if (typeof text === "string") parts.push(text);
629
+ }
630
+ }
631
+ }
632
+ return parts.join("\n");
633
+ }
634
+ function extractOutputText3(response) {
635
+ if (!response) return "";
636
+ const candidates = response.candidates;
637
+ if (!Array.isArray(candidates) || candidates.length === 0) return "";
638
+ const first = candidates[0];
639
+ const parts = first.content?.parts;
640
+ if (!Array.isArray(parts)) return "";
641
+ return parts.map((p) => typeof p.text === "string" ? p.text : "").join("");
642
+ }
643
+ function parseGemini(args) {
644
+ const { request, response, isStream, modelHint } = args;
645
+ const model = response?.modelVersion ?? modelHint ?? request?.model ?? "unknown";
646
+ const usage = response?.usageMetadata ?? {};
647
+ const inputTokens = usage.promptTokenCount ?? 0;
648
+ const outputTokens = usage.candidatesTokenCount ?? 0;
649
+ const totalTokens = usage.totalTokenCount ?? inputTokens + outputTokens;
650
+ const cost = lookupCost3(model);
651
+ return {
652
+ model,
653
+ provider: "gemini",
654
+ inputTokens,
655
+ outputTokens,
656
+ totalTokens,
657
+ costUsd: inputTokens * cost.input + outputTokens * cost.output,
658
+ inputText: extractInputText3(request),
659
+ outputText: extractOutputText3(response),
660
+ isStream
661
+ };
662
+ }
663
+
664
+ // src/parsers/mistral.ts
665
+ var MISTRAL_COSTS = {
666
+ "mistral-large": { input: 2e-6, output: 6e-6 },
667
+ "mistral-medium": { input: 27e-7, output: 81e-7 },
668
+ "mistral-small": { input: 2e-7, output: 6e-7 },
669
+ "open-mistral-7b": { input: 25e-8, output: 25e-8 },
670
+ "open-mixtral-8x7b": { input: 7e-7, output: 7e-7 },
671
+ "open-mixtral-8x22b": { input: 2e-6, output: 6e-6 }
672
+ };
673
+ function lookupCost4(model) {
674
+ if (MISTRAL_COSTS[model]) return MISTRAL_COSTS[model];
675
+ for (const key of Object.keys(MISTRAL_COSTS)) {
676
+ if (model.startsWith(key)) return MISTRAL_COSTS[key];
677
+ }
678
+ return { input: 0, output: 0 };
679
+ }
680
+ function extractInputText4(request) {
681
+ if (!request) return "";
682
+ const messages = request.messages;
683
+ if (Array.isArray(messages)) {
684
+ return messages.map((m) => {
685
+ const content = m.content;
686
+ return typeof content === "string" ? content : "";
687
+ }).join("\n");
688
+ }
689
+ return "";
690
+ }
691
+ function extractOutputText4(response) {
692
+ if (!response) return "";
693
+ const choices = response.choices;
694
+ if (Array.isArray(choices) && choices.length > 0) {
695
+ const first = choices[0];
696
+ if (first.message?.content) return first.message.content;
697
+ }
698
+ return "";
699
+ }
700
+ function parseMistral(args) {
701
+ const { request, response, isStream } = args;
702
+ const model = response?.model ?? request?.model ?? "unknown";
703
+ const usage = response?.usage ?? {};
704
+ const inputTokens = usage.prompt_tokens ?? 0;
705
+ const outputTokens = usage.completion_tokens ?? 0;
706
+ const totalTokens = usage.total_tokens ?? inputTokens + outputTokens;
707
+ const cost = lookupCost4(model);
708
+ return {
709
+ model,
710
+ provider: "mistral",
711
+ inputTokens,
712
+ outputTokens,
713
+ totalTokens,
714
+ costUsd: inputTokens * cost.input + outputTokens * cost.output,
715
+ inputText: extractInputText4(request),
716
+ outputText: extractOutputText4(response),
717
+ isStream
718
+ };
719
+ }
720
+
721
+ // src/parsers/openai.ts
722
+ var OPENAI_COSTS = {
723
+ "gpt-4o": { input: 25e-7, output: 1e-5 },
724
+ "gpt-4o-mini": { input: 15e-8, output: 6e-7 },
725
+ "gpt-4-turbo": { input: 1e-5, output: 3e-5 },
726
+ "gpt-4": { input: 3e-5, output: 6e-5 },
727
+ "gpt-3.5-turbo": { input: 5e-7, output: 15e-7 },
728
+ "text-embedding-3-small": { input: 2e-8, output: 0 },
729
+ "text-embedding-3-large": { input: 13e-8, output: 0 }
730
+ };
731
+ function lookupCost5(model) {
732
+ if (OPENAI_COSTS[model]) return OPENAI_COSTS[model];
733
+ for (const key of Object.keys(OPENAI_COSTS)) {
734
+ if (model.startsWith(key)) return OPENAI_COSTS[key];
735
+ }
736
+ return { input: 0, output: 0 };
737
+ }
738
+ function extractInputText5(request) {
739
+ if (!request) return "";
740
+ const messages = request.messages;
741
+ if (Array.isArray(messages)) {
742
+ return messages.map((m) => {
743
+ const content = m.content;
744
+ if (typeof content === "string") return content;
745
+ if (Array.isArray(content)) {
746
+ return content.map((part) => typeof part.text === "string" ? part.text : "").join("");
747
+ }
748
+ return "";
749
+ }).join("\n");
750
+ }
751
+ if (typeof request.prompt === "string") return request.prompt;
752
+ if (Array.isArray(request.input)) return request.input.map((x) => String(x)).join("\n");
753
+ if (typeof request.input === "string") return request.input;
754
+ return "";
755
+ }
756
+ function extractOutputText5(response) {
757
+ if (!response) return "";
758
+ const choices = response.choices;
759
+ if (Array.isArray(choices) && choices.length > 0) {
760
+ const first = choices[0];
761
+ if (first.message?.content) return first.message.content;
762
+ if (typeof first.text === "string") return first.text;
763
+ }
764
+ return "";
765
+ }
766
+ function parseOpenAI(args) {
767
+ const { request, response, isStream } = args;
768
+ const model = response?.model ?? request?.model ?? "unknown";
769
+ const usage = response?.usage ?? {};
770
+ const inputTokens = usage.prompt_tokens ?? 0;
771
+ const outputTokens = usage.completion_tokens ?? 0;
772
+ const totalTokens = usage.total_tokens ?? inputTokens + outputTokens;
773
+ const cost = lookupCost5(model);
774
+ const costUsd = inputTokens * cost.input + outputTokens * cost.output;
775
+ return {
776
+ model,
777
+ provider: "openai",
778
+ inputTokens,
779
+ outputTokens,
780
+ totalTokens,
781
+ costUsd,
782
+ inputText: extractInputText5(request),
783
+ outputText: extractOutputText5(response),
784
+ isStream
785
+ };
786
+ }
787
+
788
+ // src/parsers/index.ts
789
+ function parseSpan(args) {
790
+ switch (args.parser) {
791
+ case "openai":
792
+ return parseOpenAI(args);
793
+ case "anthropic":
794
+ return parseAnthropic(args);
795
+ case "gemini":
796
+ return parseGemini(args);
797
+ case "cohere":
798
+ return parseCohere(args);
799
+ case "mistral":
800
+ return parseMistral(args);
801
+ }
802
+ }
803
+
804
+ // src/pii/scrubber.ts
805
+ var PII_PATTERNS = [
806
+ { name: "email", pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replacement: "[EMAIL]" },
807
+ { name: "apikey", pattern: /\b(sk|pk|key|token|secret|api[-_]?key)[-_]?[a-zA-Z0-9]{20,}/gi, replacement: "[API_KEY]" },
808
+ { name: "creditcard", pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replacement: "[CARD]" },
809
+ { name: "ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[SSN]" },
810
+ { name: "phone", pattern: /(\+?\d{1,3}[\s-]?)?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}/g, replacement: "[PHONE]" },
811
+ { name: "ipv4", pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, replacement: "[IP]" }
812
+ ];
813
+ function scrubPII(text) {
814
+ let result = text;
815
+ for (const { pattern, replacement } of PII_PATTERNS) {
816
+ result = result.replace(pattern, replacement);
817
+ }
818
+ return result;
819
+ }
820
+ function scrubObject(obj) {
821
+ if (typeof obj === "string") return scrubPII(obj);
822
+ if (Array.isArray(obj)) return obj.map(scrubObject);
823
+ if (obj && typeof obj === "object") {
824
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, scrubObject(v)]));
825
+ }
826
+ return obj;
827
+ }
828
+
829
+ // src/transport.ts
830
+ var DEFAULT_FLUSH_INTERVAL_MS = 500;
831
+ var DEFAULT_MAX_BATCH_SIZE = 50;
832
+ var MAX_RETRIES = 3;
833
+ var Transport = class {
834
+ config;
835
+ queue = [];
836
+ timer = null;
837
+ flushing = false;
838
+ exitHandlerRegistered = false;
839
+ constructor(config) {
840
+ this.config = {
841
+ apiKey: config.apiKey,
842
+ endpoint: config.endpoint,
843
+ flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
844
+ maxBatchSize: config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
845
+ debug: config.debug ?? false,
846
+ pii: config.pii ?? true
847
+ };
848
+ this.startTimer();
849
+ this.registerExitHandler();
850
+ }
851
+ push(payload) {
852
+ let parsed;
853
+ try {
854
+ parsed = parseSpan({
855
+ parser: payload.parser,
856
+ request: payload.request ?? null,
857
+ response: payload.response ?? null,
858
+ isStream: payload.isStream
859
+ });
860
+ } catch (err) {
861
+ if (this.config.debug) {
862
+ console.warn("[agentlens] parser failed", err);
863
+ }
864
+ return;
865
+ }
866
+ const outbound = this.toOutbound(parsed, payload.latency, payload.status);
867
+ this.enqueue(outbound);
868
+ }
869
+ pushError(payload) {
870
+ const ids = this.resolveIds();
871
+ const span = {
872
+ spanId: ids.spanId,
873
+ traceId: ids.traceId,
874
+ parentSpanId: ids.parentSpanId,
875
+ model: "unknown",
876
+ provider: payload.provider,
877
+ inputTokens: 0,
878
+ outputTokens: 0,
879
+ totalTokens: 0,
880
+ costUsd: 0,
881
+ inputText: this.scrub(this.stringifyRequest(payload.request)),
882
+ outputText: "",
883
+ isStream: false,
884
+ error: payload.error,
885
+ latency: payload.latency,
886
+ status: 0,
887
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
888
+ };
889
+ this.enqueue(span);
890
+ }
891
+ async flush() {
892
+ if (this.flushing) return;
893
+ if (this.queue.length === 0) return;
894
+ this.flushing = true;
895
+ const batch = this.queue.splice(0, this.queue.length);
896
+ try {
897
+ await this.send(batch);
898
+ } catch {
899
+ } finally {
900
+ this.flushing = false;
901
+ }
902
+ }
903
+ shutdown() {
904
+ if (this.timer) {
905
+ clearInterval(this.timer);
906
+ this.timer = null;
907
+ }
908
+ }
909
+ toOutbound(parsed, latency, status) {
910
+ const ids = this.resolveIds();
911
+ return {
912
+ ...parsed,
913
+ spanId: ids.spanId,
914
+ traceId: ids.traceId,
915
+ parentSpanId: ids.parentSpanId,
916
+ inputText: this.scrub(parsed.inputText),
917
+ outputText: this.scrub(parsed.outputText),
918
+ latency,
919
+ status,
920
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
921
+ };
922
+ }
923
+ /**
924
+ * Resolves the trace-context IDs to stamp on an outbound span.
925
+ *
926
+ * - `spanId` is always fresh per LLM call (the call IS the leaf span).
927
+ * - `traceId` reuses the surrounding `trace()` block's traceId if any,
928
+ * else a new one (the standalone LLM call is its own single-span trace).
929
+ * - `parentSpanId` is the surrounding trace's currentSpanId, or undefined
930
+ * when there is no enclosing `trace()`.
931
+ *
932
+ * AsyncLocalStorage propagates through the patched fetch's promise chain,
933
+ * so this still resolves correctly for streamed spans emitted from the
934
+ * background `captureSSEStream()` reader.
935
+ */
936
+ resolveIds() {
937
+ return {
938
+ spanId: crypto.randomUUID(),
939
+ traceId: agentlensCore.getCurrentTraceId() ?? crypto.randomUUID(),
940
+ parentSpanId: agentlensCore.getCurrentSpanId()
941
+ };
942
+ }
943
+ scrub(text) {
944
+ if (!this.config.pii) return text;
945
+ return scrubPII(text);
946
+ }
947
+ stringifyRequest(request) {
948
+ if (request == null) return "";
949
+ if (typeof request === "string") return request;
950
+ try {
951
+ return JSON.stringify(request);
952
+ } catch {
953
+ return "";
954
+ }
955
+ }
956
+ enqueue(span) {
957
+ if (this.config.debug) {
958
+ console.log("[agentlens] span", {
959
+ model: span.model,
960
+ provider: span.provider,
961
+ inputTokens: span.inputTokens,
962
+ outputTokens: span.outputTokens,
963
+ costUsd: span.costUsd,
964
+ latency: span.latency,
965
+ isStream: span.isStream
966
+ });
967
+ }
968
+ this.queue.push(span);
969
+ if (this.queue.length >= this.config.maxBatchSize) {
970
+ void this.flush();
971
+ }
972
+ }
973
+ startTimer() {
974
+ if (this.timer) return;
975
+ this.timer = setInterval(() => {
976
+ void this.flush();
977
+ }, this.config.flushIntervalMs);
978
+ if (typeof this.timer.unref === "function") {
979
+ this.timer.unref();
980
+ }
981
+ }
982
+ registerExitHandler() {
983
+ if (this.exitHandlerRegistered) return;
984
+ if (typeof process === "undefined" || typeof process.on !== "function") return;
985
+ this.exitHandlerRegistered = true;
986
+ process.on("beforeExit", () => {
987
+ void this.flush();
988
+ });
989
+ }
990
+ async send(batch) {
991
+ const body = JSON.stringify({ spans: batch });
992
+ let attempt = 0;
993
+ let delay = 100;
994
+ while (attempt < MAX_RETRIES) {
995
+ try {
996
+ const res = await this.doFetch(body);
997
+ if (res.ok) return;
998
+ if (res.status >= 400 && res.status < 500) {
999
+ if (this.config.debug) {
1000
+ console.warn("[agentlens] flush rejected", res.status);
1001
+ }
1002
+ return;
1003
+ }
1004
+ } catch {
1005
+ }
1006
+ attempt++;
1007
+ if (attempt < MAX_RETRIES) {
1008
+ await sleep(delay);
1009
+ delay *= 2;
1010
+ }
1011
+ }
1012
+ if (this.config.debug) {
1013
+ console.warn("[agentlens] flush gave up after", MAX_RETRIES, "attempts");
1014
+ }
1015
+ }
1016
+ async doFetch(body) {
1017
+ const f = globalThis.__agentlens_originalFetch ?? globalThis.fetch;
1018
+ if (!f) {
1019
+ throw new Error("fetch unavailable");
1020
+ }
1021
+ const res = await f(this.config.endpoint, {
1022
+ method: "POST",
1023
+ headers: {
1024
+ "content-type": "application/json",
1025
+ authorization: `Bearer ${this.config.apiKey}`
1026
+ },
1027
+ body
1028
+ });
1029
+ return { ok: res.ok, status: res.status };
1030
+ }
1031
+ };
1032
+ function sleep(ms) {
1033
+ return new Promise((resolve) => setTimeout(resolve, ms));
1034
+ }
1035
+ var DEFAULT_ENDPOINT = "https://ingest.agentlens.dev/v1/spans";
1036
+ var initialized = false;
1037
+ var transport = null;
1038
+ var AgentLens = {
1039
+ init(config) {
1040
+ if (initialized) return;
1041
+ if (!config?.apiKey) {
1042
+ throw new Error("AgentLens.init requires an apiKey");
1043
+ }
1044
+ initialized = true;
1045
+ transport = new Transport({
1046
+ apiKey: config.apiKey,
1047
+ endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
1048
+ debug: config.debug ?? false,
1049
+ pii: config.pii ?? true,
1050
+ flushIntervalMs: config.flushIntervalMs,
1051
+ maxBatchSize: config.maxBatchSize
1052
+ });
1053
+ patchFetch(transport);
1054
+ patchHttps(transport);
1055
+ },
1056
+ async flush() {
1057
+ if (!transport) return;
1058
+ await transport.flush();
1059
+ },
1060
+ shutdown() {
1061
+ if (!transport) return;
1062
+ transport.shutdown();
1063
+ transport = null;
1064
+ initialized = false;
1065
+ },
1066
+ /**
1067
+ * Wraps `fn` in a named trace context. Any LLM calls made inside `fn`
1068
+ * (including async ones) are auto-tagged with this trace's `traceId` and
1069
+ * get `parentSpanId` set to this trace's `spanId`. Nested `trace()` calls
1070
+ * become child spans automatically.
1071
+ *
1072
+ * Use this to group multiple LLM calls that belong to the same agent run.
1073
+ */
1074
+ trace(_name, fn) {
1075
+ const spanId = crypto.randomUUID();
1076
+ const traceId = agentlensCore.getCurrentTraceId() ?? crypto.randomUUID();
1077
+ return agentlensCore.runWithTrace({ traceId, currentSpanId: spanId }, fn);
1078
+ }
1079
+ };
1080
+
1081
+ Object.defineProperty(exports, "getCurrentSpanId", {
1082
+ enumerable: true,
1083
+ get: function () { return agentlensCore.getCurrentSpanId; }
1084
+ });
1085
+ Object.defineProperty(exports, "getCurrentTrace", {
1086
+ enumerable: true,
1087
+ get: function () { return agentlensCore.getCurrentTrace; }
1088
+ });
1089
+ Object.defineProperty(exports, "getCurrentTraceId", {
1090
+ enumerable: true,
1091
+ get: function () { return agentlensCore.getCurrentTraceId; }
1092
+ });
1093
+ Object.defineProperty(exports, "runWithTrace", {
1094
+ enumerable: true,
1095
+ get: function () { return agentlensCore.runWithTrace; }
1096
+ });
1097
+ exports.AgentLens = AgentLens;
1098
+ exports.LLM_REGISTRY = LLM_REGISTRY;
1099
+ exports.matchLLM = matchLLM;
1100
+ exports.scrubObject = scrubObject;
1101
+ exports.scrubPII = scrubPII;
1102
+ //# sourceMappingURL=index.js.map
1103
+ //# sourceMappingURL=index.js.map