@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/README.md +151 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +1103 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1082 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
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
|