context-lens 0.2.0 → 0.3.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 +120 -25
- package/dist/cli-utils.d.ts +2 -1
- package/dist/cli-utils.d.ts.map +1 -1
- package/dist/cli-utils.js +40 -13
- package/dist/cli-utils.js.map +1 -1
- package/dist/cli.js +184 -68
- package/dist/cli.js.map +1 -1
- package/dist/core/conversation.d.ts +63 -0
- package/dist/core/conversation.d.ts.map +1 -0
- package/dist/core/conversation.js +305 -0
- package/dist/core/conversation.js.map +1 -0
- package/dist/core/health.d.ts +7 -0
- package/dist/core/health.d.ts.map +1 -0
- package/dist/core/health.js +311 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/models.d.ts +36 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +111 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/parse.d.ts +17 -0
- package/dist/core/parse.d.ts.map +1 -0
- package/dist/core/parse.js +349 -0
- package/dist/core/parse.js.map +1 -0
- package/dist/core/routing.d.ts +47 -0
- package/dist/core/routing.d.ts.map +1 -0
- package/dist/core/routing.js +132 -0
- package/dist/core/routing.js.map +1 -0
- package/dist/core/security.d.ts +8 -0
- package/dist/core/security.d.ts.map +1 -0
- package/dist/core/security.js +222 -0
- package/dist/core/security.js.map +1 -0
- package/dist/core/source.d.ts +22 -0
- package/dist/core/source.d.ts.map +1 -0
- package/dist/core/source.js +56 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/tokens.d.ts +29 -0
- package/dist/core/tokens.d.ts.map +1 -0
- package/dist/core/tokens.js +163 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/core.d.ts +14 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +14 -471
- package/dist/core.js.map +1 -1
- package/dist/http/headers.d.ts +25 -0
- package/dist/http/headers.d.ts.map +1 -0
- package/dist/http/headers.js +54 -0
- package/dist/http/headers.js.map +1 -0
- package/dist/lhar/composition.d.ts +12 -0
- package/dist/lhar/composition.d.ts.map +1 -0
- package/dist/lhar/composition.js +258 -0
- package/dist/lhar/composition.js.map +1 -0
- package/dist/lhar/export.d.ts +5 -0
- package/dist/lhar/export.d.ts.map +1 -0
- package/dist/lhar/export.js +59 -0
- package/dist/lhar/export.js.map +1 -0
- package/dist/lhar/record.d.ts +6 -0
- package/dist/lhar/record.d.ts.map +1 -0
- package/dist/lhar/record.js +216 -0
- package/dist/lhar/record.js.map +1 -0
- package/dist/lhar/response.d.ts +11 -0
- package/dist/lhar/response.d.ts.map +1 -0
- package/dist/lhar/response.js +132 -0
- package/dist/lhar/response.js.map +1 -0
- package/dist/lhar-types.generated.d.ts +24 -3
- package/dist/lhar-types.generated.d.ts.map +1 -1
- package/dist/lhar.d.ts +12 -19
- package/dist/lhar.d.ts.map +1 -1
- package/dist/lhar.js +16 -473
- package/dist/lhar.js.map +1 -1
- package/dist/server/api.d.ts +8 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +292 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/config.d.ts +13 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +36 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/projection.d.ts +9 -0
- package/dist/server/projection.d.ts.map +1 -0
- package/dist/server/projection.js +47 -0
- package/dist/server/projection.js.map +1 -0
- package/dist/server/proxy.d.ts +13 -0
- package/dist/server/proxy.d.ts.map +1 -0
- package/dist/server/proxy.js +218 -0
- package/dist/server/proxy.js.map +1 -0
- package/dist/server/static.d.ts +9 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +78 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/store.d.ts +81 -0
- package/dist/server/store.d.ts.map +1 -0
- package/dist/server/store.js +632 -0
- package/dist/server/store.js.map +1 -0
- package/dist/server/webui.d.ts +5 -0
- package/dist/server/webui.d.ts.map +1 -0
- package/dist/server/webui.js +42 -0
- package/dist/server/webui.js.map +1 -0
- package/dist/server-utils.d.ts +2 -2
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +12 -21
- package/dist/server-utils.js.map +1 -1
- package/dist/server.js +31 -697
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +94 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/version.generated.d.ts +2 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +2 -0
- package/dist/version.generated.js.map +1 -0
- package/findings-screenshot.png +0 -0
- package/messages-screenshot.png +0 -0
- package/package.json +23 -10
- package/schema/lhar.schema.json +58 -4
- package/screenshot-overview.png +0 -0
- package/sessions-screenshot.png +0 -0
- package/timeline-screenshot.png +0 -0
- package/diff.png +0 -0
- package/overview-sidebar.png +0 -0
- package/public/index.html +0 -2804
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared header redaction utilities.
|
|
3
|
+
*
|
|
4
|
+
* Context Lens captures and exports some headers for debugging and provenance,
|
|
5
|
+
* but must never persist secrets (API keys, cookies, auth challenges, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Keep this as the single source of truth to avoid drift between "capture" and "export".
|
|
8
|
+
*/
|
|
9
|
+
/** Case-insensitive set of header names that must never be persisted/exported. */
|
|
10
|
+
export const SENSITIVE_HEADERS = new Set([
|
|
11
|
+
"authorization",
|
|
12
|
+
"x-api-key",
|
|
13
|
+
"cookie",
|
|
14
|
+
"set-cookie",
|
|
15
|
+
"x-target-url",
|
|
16
|
+
"proxy-authorization",
|
|
17
|
+
"x-auth-token",
|
|
18
|
+
"x-forwarded-authorization",
|
|
19
|
+
"www-authenticate",
|
|
20
|
+
"proxy-authenticate",
|
|
21
|
+
"x-goog-api-key",
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Remove sensitive headers from a header map.
|
|
25
|
+
*
|
|
26
|
+
* @param headers - Header map (string -> string)
|
|
27
|
+
* @returns A new object with sensitive headers removed.
|
|
28
|
+
*/
|
|
29
|
+
export function redactHeaders(headers) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
32
|
+
if (SENSITIVE_HEADERS.has(key.toLowerCase()))
|
|
33
|
+
continue;
|
|
34
|
+
result[key] = val;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Select a safe subset of request/response headers for capture.
|
|
40
|
+
*
|
|
41
|
+
* - Drops sensitive headers (see `SENSITIVE_HEADERS`)
|
|
42
|
+
* - Keeps only string-valued headers (Node can represent multi-valued headers as arrays)
|
|
43
|
+
*/
|
|
44
|
+
export function selectHeaders(headers) {
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
47
|
+
if (SENSITIVE_HEADERS.has(key.toLowerCase()))
|
|
48
|
+
continue;
|
|
49
|
+
if (typeof val === "string")
|
|
50
|
+
result[key] = val;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/http/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IACvC,eAAe;IACf,WAAW;IACX,QAAQ;IACR,YAAY;IACZ,cAAc;IACd,qBAAqB;IACrB,cAAc;IACd,2BAA2B;IAC3B,kBAAkB;IAClB,oBAAoB;IACpB,gBAAgB;CACjB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,OAA+B;IAE/B,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACjD,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAAE,SAAS;QACvD,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,OAA4B;IAE5B,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACjD,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAAE,SAAS;QACvD,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACjD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CompositionEntry } from "../lhar-types.generated.js";
|
|
2
|
+
import type { ContextInfo } from "../types.js";
|
|
3
|
+
export declare function analyzeComposition(contextInfo: ContextInfo, rawBody: Record<string, any> | undefined): CompositionEntry[];
|
|
4
|
+
/**
|
|
5
|
+
* Normalize composition token counts so their sum equals an authoritative total.
|
|
6
|
+
*
|
|
7
|
+
* Scales each entry proportionally and applies a rounding residual fix on the
|
|
8
|
+
* largest entry so `sum(composition[].tokens) === authoritative` exactly.
|
|
9
|
+
* Also recomputes `pct` fields.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeComposition(composition: CompositionEntry[], authoritative: number): void;
|
|
12
|
+
//# sourceMappingURL=composition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../../src/lhar/composition.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GACvC,gBAAgB,EAAE,CAwEpB;AA8KD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,gBAAgB,EAAE,EAC/B,aAAa,EAAE,MAAM,GACpB,IAAI,CAgCN"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { estimateTokens } from "../core.js";
|
|
2
|
+
export function analyzeComposition(contextInfo, rawBody) {
|
|
3
|
+
const counts = new Map();
|
|
4
|
+
function add(category, tokens) {
|
|
5
|
+
const existing = counts.get(category);
|
|
6
|
+
if (existing) {
|
|
7
|
+
existing.tokens += tokens;
|
|
8
|
+
existing.count += 1;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
counts.set(category, { tokens, count: 1 });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!rawBody) {
|
|
15
|
+
// Fallback to contextInfo aggregates
|
|
16
|
+
if (contextInfo.systemTokens > 0)
|
|
17
|
+
add("system_prompt", contextInfo.systemTokens);
|
|
18
|
+
if (contextInfo.toolsTokens > 0)
|
|
19
|
+
add("tool_definitions", contextInfo.toolsTokens);
|
|
20
|
+
if (contextInfo.messagesTokens > 0)
|
|
21
|
+
add("other", contextInfo.messagesTokens);
|
|
22
|
+
return buildCompositionArray(counts, contextInfo.totalTokens);
|
|
23
|
+
}
|
|
24
|
+
// System prompt(s)
|
|
25
|
+
if (rawBody.system) {
|
|
26
|
+
if (typeof rawBody.system === "string") {
|
|
27
|
+
add("system_prompt", estimateTokens(rawBody.system));
|
|
28
|
+
}
|
|
29
|
+
else if (Array.isArray(rawBody.system)) {
|
|
30
|
+
for (const block of rawBody.system) {
|
|
31
|
+
// cache_control metadata is negligible; count only the text content
|
|
32
|
+
add("system_prompt", estimateTokens(block.text || block));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Instructions (OpenAI Responses API / ChatGPT)
|
|
37
|
+
if (rawBody.instructions) {
|
|
38
|
+
add("system_prompt", estimateTokens(rawBody.instructions));
|
|
39
|
+
}
|
|
40
|
+
// Tool definitions
|
|
41
|
+
if (rawBody.tools && Array.isArray(rawBody.tools)) {
|
|
42
|
+
add("tool_definitions", estimateTokens(JSON.stringify(rawBody.tools)));
|
|
43
|
+
}
|
|
44
|
+
// Gemini/Code Assist: unwrap .request wrapper if present
|
|
45
|
+
const geminiBody = rawBody.request || rawBody;
|
|
46
|
+
// Gemini systemInstruction
|
|
47
|
+
if (geminiBody.systemInstruction) {
|
|
48
|
+
const parts = geminiBody.systemInstruction.parts || [];
|
|
49
|
+
add("system_prompt", estimateTokens(parts.map((p) => p.text || "").join("\n")));
|
|
50
|
+
}
|
|
51
|
+
// Gemini contents[] or standard messages[]/input[]
|
|
52
|
+
const messages = geminiBody.contents || rawBody.messages || rawBody.input;
|
|
53
|
+
if (Array.isArray(messages)) {
|
|
54
|
+
for (const msg of messages) {
|
|
55
|
+
classifyMessage(msg, add);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (typeof messages === "string") {
|
|
59
|
+
add("user_text", estimateTokens(messages));
|
|
60
|
+
}
|
|
61
|
+
const total = Array.from(counts.values()).reduce((s, e) => s + e.tokens, 0);
|
|
62
|
+
return buildCompositionArray(counts, total);
|
|
63
|
+
}
|
|
64
|
+
function classifyMessage(msg, add) {
|
|
65
|
+
const type = msg.type || "";
|
|
66
|
+
// OpenAI Responses API typed items (no role field)
|
|
67
|
+
if (!msg.role && type) {
|
|
68
|
+
if (type === "function_call" || type === "custom_tool_call") {
|
|
69
|
+
add("tool_calls", estimateTokens(msg));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (type === "function_call_output" || type === "custom_tool_call_output") {
|
|
73
|
+
add("tool_results", estimateTokens(msg.output || ""));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (type === "reasoning") {
|
|
77
|
+
add("thinking", estimateTokens(msg));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (type === "output_text") {
|
|
81
|
+
add("assistant_text", estimateTokens(msg.text || ""));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (type === "input_text") {
|
|
85
|
+
add("user_text", estimateTokens(msg.text || ""));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const role = msg.role || "user";
|
|
90
|
+
const content = msg.content;
|
|
91
|
+
// System / developer messages
|
|
92
|
+
if (role === "system" || role === "developer") {
|
|
93
|
+
add("system_prompt", estimateTokens(content));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// String content
|
|
97
|
+
if (typeof content === "string") {
|
|
98
|
+
if (content.includes("<system-reminder>")) {
|
|
99
|
+
add("system_injections", estimateTokens(content));
|
|
100
|
+
}
|
|
101
|
+
else if (role === "assistant") {
|
|
102
|
+
add("assistant_text", estimateTokens(content));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
add("user_text", estimateTokens(content));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Gemini parts array (role + parts instead of role + content)
|
|
110
|
+
if (msg.parts && Array.isArray(msg.parts)) {
|
|
111
|
+
for (const part of msg.parts) {
|
|
112
|
+
classifyGeminiPart(part, role, add);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Array of content blocks
|
|
117
|
+
if (Array.isArray(content)) {
|
|
118
|
+
for (const block of content) {
|
|
119
|
+
classifyBlock(block, role, add);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Fallback
|
|
124
|
+
if (content) {
|
|
125
|
+
add("other", estimateTokens(content));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function classifyBlock(block, role, add) {
|
|
129
|
+
const type = block.type || "";
|
|
130
|
+
if (type === "tool_use") {
|
|
131
|
+
add("tool_calls", estimateTokens(block));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (type === "tool_result") {
|
|
135
|
+
add("tool_results", estimateTokens(block.content || ""));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (type === "thinking") {
|
|
139
|
+
add("thinking", estimateTokens(block.thinking || block.text || ""));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (type === "image" || type === "image_url") {
|
|
143
|
+
add("images", estimateTokens(block)); // estimateTokens handles image blocks with fixed estimate
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Text blocks
|
|
147
|
+
const text = block.text || "";
|
|
148
|
+
if (type === "text" || type === "input_text" || !type) {
|
|
149
|
+
if (text.includes("<system-reminder>")) {
|
|
150
|
+
add("system_injections", estimateTokens(text));
|
|
151
|
+
}
|
|
152
|
+
else if (block.cache_control) {
|
|
153
|
+
// Count text in its natural category only; the cache_control metadata
|
|
154
|
+
// itself is negligible overhead and should not inflate the total.
|
|
155
|
+
if (role === "assistant") {
|
|
156
|
+
add("assistant_text", estimateTokens(text));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
add("user_text", estimateTokens(text));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (role === "assistant") {
|
|
163
|
+
add("assistant_text", estimateTokens(text));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
add("user_text", estimateTokens(text));
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
add("other", estimateTokens(block));
|
|
171
|
+
}
|
|
172
|
+
function classifyGeminiPart(part, role, add) {
|
|
173
|
+
if (part.text) {
|
|
174
|
+
if (role === "model") {
|
|
175
|
+
add("assistant_text", estimateTokens(part.text));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
add("user_text", estimateTokens(part.text));
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (part.functionCall) {
|
|
183
|
+
add("tool_calls", estimateTokens(part.functionCall));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (part.functionResponse) {
|
|
187
|
+
add("tool_results", estimateTokens(part.functionResponse));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (part.inlineData || part.fileData) {
|
|
191
|
+
add("images", estimateTokens(part));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (part.executableCode || part.codeExecutionResult) {
|
|
195
|
+
add("assistant_text", estimateTokens(part));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
add("other", estimateTokens(part));
|
|
199
|
+
}
|
|
200
|
+
function buildCompositionArray(counts, total) {
|
|
201
|
+
const result = [];
|
|
202
|
+
for (const [category, { tokens, count }] of counts) {
|
|
203
|
+
if (tokens === 0)
|
|
204
|
+
continue;
|
|
205
|
+
result.push({
|
|
206
|
+
category,
|
|
207
|
+
tokens,
|
|
208
|
+
pct: total > 0 ? Math.round((tokens / total) * 1000) / 10 : 0,
|
|
209
|
+
count,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Sort by tokens descending
|
|
213
|
+
result.sort((a, b) => b.tokens - a.tokens);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Normalize composition token counts so their sum equals an authoritative total.
|
|
218
|
+
*
|
|
219
|
+
* Scales each entry proportionally and applies a rounding residual fix on the
|
|
220
|
+
* largest entry so `sum(composition[].tokens) === authoritative` exactly.
|
|
221
|
+
* Also recomputes `pct` fields.
|
|
222
|
+
*/
|
|
223
|
+
export function normalizeComposition(composition, authoritative) {
|
|
224
|
+
if (composition.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
const rawSum = composition.reduce((s, c) => s + c.tokens, 0);
|
|
227
|
+
if (rawSum === 0 || authoritative === 0)
|
|
228
|
+
return;
|
|
229
|
+
if (rawSum === authoritative) {
|
|
230
|
+
// Already matches; just recompute pct for consistency
|
|
231
|
+
for (const c of composition) {
|
|
232
|
+
c.pct =
|
|
233
|
+
authoritative > 0
|
|
234
|
+
? Math.round((c.tokens / authoritative) * 1000) / 10
|
|
235
|
+
: 0;
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const scale = authoritative / rawSum;
|
|
240
|
+
let running = 0;
|
|
241
|
+
for (const c of composition) {
|
|
242
|
+
c.tokens = Math.round(c.tokens * scale);
|
|
243
|
+
running += c.tokens;
|
|
244
|
+
}
|
|
245
|
+
// Fix rounding residual on the first (largest) entry
|
|
246
|
+
const residual = authoritative - running;
|
|
247
|
+
if (residual !== 0) {
|
|
248
|
+
composition[0].tokens += residual;
|
|
249
|
+
}
|
|
250
|
+
// Recompute pct
|
|
251
|
+
for (const c of composition) {
|
|
252
|
+
c.pct =
|
|
253
|
+
authoritative > 0
|
|
254
|
+
? Math.round((c.tokens / authoritative) * 1000) / 10
|
|
255
|
+
: 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/lhar/composition.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LharJsonWrapper } from "../lhar-types.generated.js";
|
|
2
|
+
import type { CapturedEntry, Conversation, PrivacyLevel } from "../types.js";
|
|
3
|
+
export declare function toLharJsonl(entries: CapturedEntry[], conversations: Map<string, Conversation>, privacy?: PrivacyLevel): string;
|
|
4
|
+
export declare function toLharJson(entries: CapturedEntry[], conversations: Map<string, Conversation>, privacy?: PrivacyLevel): LharJsonWrapper;
|
|
5
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/lhar/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAY7E,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EAAE,EACxB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACxC,OAAO,GAAE,YAAyB,GACjC,MAAM,CAmCR;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,aAAa,EAAE,EACxB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACxC,OAAO,GAAE,YAAyB,GACjC,eAAe,CAwCjB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { VERSION } from "../version.generated.js";
|
|
2
|
+
import { buildLharRecord, buildSessionLine, traceIdFromConversation, } from "./record.js";
|
|
3
|
+
const COLLECTOR_NAME = "context-lens";
|
|
4
|
+
const COLLECTOR_VERSION = VERSION;
|
|
5
|
+
const LHAR_VERSION = "0.1.0";
|
|
6
|
+
export function toLharJsonl(entries, conversations, privacy = "standard") {
|
|
7
|
+
// Sort oldest-first for JSONL
|
|
8
|
+
const sorted = [...entries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
9
|
+
const lines = [];
|
|
10
|
+
const emittedSessions = new Set();
|
|
11
|
+
for (const entry of sorted) {
|
|
12
|
+
const record = buildLharRecord(entry, entries, privacy);
|
|
13
|
+
// Emit session preamble on first occurrence of each trace_id
|
|
14
|
+
if (!emittedSessions.has(record.trace_id)) {
|
|
15
|
+
emittedSessions.add(record.trace_id);
|
|
16
|
+
const convo = entry.conversationId
|
|
17
|
+
? conversations.get(entry.conversationId)
|
|
18
|
+
: undefined;
|
|
19
|
+
if (convo) {
|
|
20
|
+
lines.push(JSON.stringify(buildSessionLine(entry.conversationId, convo, record.gen_ai.request.model)));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
lines.push(JSON.stringify(record));
|
|
24
|
+
}
|
|
25
|
+
return `${lines.join("\n")}\n`;
|
|
26
|
+
}
|
|
27
|
+
export function toLharJson(entries, conversations, privacy = "standard") {
|
|
28
|
+
const sorted = [...entries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
29
|
+
const records = sorted.map((entry) => buildLharRecord(entry, entries, privacy));
|
|
30
|
+
// Build sessions from conversations map
|
|
31
|
+
const sessions = [];
|
|
32
|
+
const seenTraces = new Set();
|
|
33
|
+
for (const record of records) {
|
|
34
|
+
if (!seenTraces.has(record.trace_id)) {
|
|
35
|
+
seenTraces.add(record.trace_id);
|
|
36
|
+
const convo = record.trace_id
|
|
37
|
+
? Array.from(conversations.values()).find((c) => traceIdFromConversation(c.id) === record.trace_id)
|
|
38
|
+
: undefined;
|
|
39
|
+
sessions.push({
|
|
40
|
+
trace_id: record.trace_id,
|
|
41
|
+
started_at: convo?.firstSeen || record.timestamp,
|
|
42
|
+
tool: record.source.tool,
|
|
43
|
+
model: record.gen_ai.request.model,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
lhar: {
|
|
49
|
+
version: LHAR_VERSION,
|
|
50
|
+
creator: {
|
|
51
|
+
name: COLLECTOR_NAME,
|
|
52
|
+
version: COLLECTOR_VERSION,
|
|
53
|
+
},
|
|
54
|
+
sessions,
|
|
55
|
+
entries: records,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/lhar/export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LharRecord, LharSessionLine } from "../lhar-types.generated.js";
|
|
2
|
+
import type { CapturedEntry, Conversation, PrivacyLevel } from "../types.js";
|
|
3
|
+
export declare function traceIdFromConversation(conversationId: string | null): string;
|
|
4
|
+
export declare function buildLharRecord(entry: CapturedEntry, prevEntries: CapturedEntry[], privacy?: PrivacyLevel): LharRecord;
|
|
5
|
+
export declare function buildSessionLine(conversationId: string, conversation: Conversation, model: string): LharSessionLine;
|
|
6
|
+
//# sourceMappingURL=record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/lhar/record.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAY7E,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAI7E;AA2BD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,WAAW,EAAE,aAAa,EAAE,EAC5B,OAAO,GAAE,YAAyB,GACjC,UAAU,CAqMZ;AAED,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,MAAM,GACZ,eAAe,CAQjB"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { redactHeaders } from "../http/headers.js";
|
|
3
|
+
import { VERSION } from "../version.generated.js";
|
|
4
|
+
import { analyzeComposition } from "./composition.js";
|
|
5
|
+
import { parseResponseUsage } from "./response.js";
|
|
6
|
+
const COLLECTOR_NAME = "context-lens";
|
|
7
|
+
const COLLECTOR_VERSION = VERSION;
|
|
8
|
+
function hexId(bytes) {
|
|
9
|
+
return randomBytes(bytes).toString("hex");
|
|
10
|
+
}
|
|
11
|
+
export function traceIdFromConversation(conversationId) {
|
|
12
|
+
if (!conversationId)
|
|
13
|
+
return hexId(16);
|
|
14
|
+
// Deterministic: hash the conversationId to a 32-hex-char trace ID
|
|
15
|
+
return createHash("sha256").update(conversationId).digest("hex").slice(0, 32);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract the response body for raw capture. Returns null if the response
|
|
19
|
+
* data doesn't contain a capturable body.
|
|
20
|
+
*/
|
|
21
|
+
function responseBodyForCapture(response) {
|
|
22
|
+
if (!response)
|
|
23
|
+
return null;
|
|
24
|
+
const resp = response;
|
|
25
|
+
// Streaming: raw SSE chunks
|
|
26
|
+
if (resp.streaming && typeof resp.chunks === "string") {
|
|
27
|
+
return resp.chunks;
|
|
28
|
+
}
|
|
29
|
+
// Raw string response
|
|
30
|
+
if (resp.raw && typeof resp.raw === "string") {
|
|
31
|
+
return resp.raw;
|
|
32
|
+
}
|
|
33
|
+
// Marker-only raw response (body wasn't captured)
|
|
34
|
+
if (resp.raw === true) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// Parsed JSON response object
|
|
38
|
+
return resp;
|
|
39
|
+
}
|
|
40
|
+
export function buildLharRecord(entry, prevEntries, privacy = "standard") {
|
|
41
|
+
const ci = entry.contextInfo;
|
|
42
|
+
// Use pre-computed composition from storeRequest; fall back to recomputing
|
|
43
|
+
const composition = entry.composition.length > 0
|
|
44
|
+
? entry.composition
|
|
45
|
+
: analyzeComposition(ci, entry.rawBody);
|
|
46
|
+
const usage = parseResponseUsage(entry.response);
|
|
47
|
+
// Sequence + growth must be derived from a stable ordering.
|
|
48
|
+
// Use oldest-first timestamp ordering within the conversation; tie-break by id.
|
|
49
|
+
let convoEntries = entry.conversationId
|
|
50
|
+
? prevEntries.filter((e) => e.conversationId === entry.conversationId)
|
|
51
|
+
: [entry];
|
|
52
|
+
// Make buildLharRecord work even if the caller doesn't include `entry` in `prevEntries`.
|
|
53
|
+
if (entry.conversationId) {
|
|
54
|
+
const found = convoEntries.some((e) => e.id === entry.id && e.timestamp === entry.timestamp);
|
|
55
|
+
if (!found)
|
|
56
|
+
convoEntries = [...convoEntries, entry];
|
|
57
|
+
}
|
|
58
|
+
convoEntries.sort((a, b) => {
|
|
59
|
+
const dt = new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
|
60
|
+
if (dt !== 0)
|
|
61
|
+
return dt;
|
|
62
|
+
return a.id - b.id;
|
|
63
|
+
});
|
|
64
|
+
let convoIndex = convoEntries.findIndex((e) => e.id === entry.id && e.timestamp === entry.timestamp);
|
|
65
|
+
if (convoIndex < 0)
|
|
66
|
+
convoIndex = convoEntries.findIndex((e) => e.id === entry.id);
|
|
67
|
+
if (convoIndex < 0)
|
|
68
|
+
convoIndex = 0;
|
|
69
|
+
const sequence = convoIndex + 1;
|
|
70
|
+
// Agent role: the most common agentKey in the conversation is the "main" agent.
|
|
71
|
+
// Entries with a different key (or null when main is non-null) are subagents.
|
|
72
|
+
let agentRole = "main";
|
|
73
|
+
let mainKey = null;
|
|
74
|
+
if (entry.conversationId && convoEntries.length > 1) {
|
|
75
|
+
const keyCounts = new Map();
|
|
76
|
+
for (const e of convoEntries) {
|
|
77
|
+
keyCounts.set(e.agentKey, (keyCounts.get(e.agentKey) || 0) + 1);
|
|
78
|
+
}
|
|
79
|
+
let maxCount = 0;
|
|
80
|
+
for (const [key, count] of keyCounts) {
|
|
81
|
+
if (count > maxCount) {
|
|
82
|
+
maxCount = count;
|
|
83
|
+
mainKey = key;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (entry.agentKey !== mainKey) {
|
|
87
|
+
agentRole = "subagent";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Growth tracking: compare to the previous same-role entry in the conversation
|
|
91
|
+
// to avoid false compaction/growth spikes from main↔subagent transitions.
|
|
92
|
+
let prevEntry = null;
|
|
93
|
+
for (let i = convoIndex - 1; i >= 0; i--) {
|
|
94
|
+
if (convoEntries[i].agentKey === entry.agentKey) {
|
|
95
|
+
prevEntry = convoEntries[i];
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const prevTokens = prevEntry ? prevEntry.contextInfo.totalTokens : 0;
|
|
100
|
+
const tokensAdded = prevEntry ? ci.totalTokens - prevTokens : null;
|
|
101
|
+
const compactionDetected = tokensAdded !== null && tokensAdded < 0;
|
|
102
|
+
// Tokens per second
|
|
103
|
+
let tokensPerSecond = null;
|
|
104
|
+
if (entry.timings && entry.timings.receive_ms > 0 && usage.outputTokens > 0) {
|
|
105
|
+
tokensPerSecond =
|
|
106
|
+
Math.round((usage.outputTokens / entry.timings.receive_ms) * 1000 * 10) /
|
|
107
|
+
10;
|
|
108
|
+
}
|
|
109
|
+
const timings = entry.timings
|
|
110
|
+
? {
|
|
111
|
+
...entry.timings,
|
|
112
|
+
tokens_per_second: tokensPerSecond,
|
|
113
|
+
}
|
|
114
|
+
: null;
|
|
115
|
+
return {
|
|
116
|
+
type: "entry",
|
|
117
|
+
id: randomUUID(),
|
|
118
|
+
trace_id: traceIdFromConversation(entry.conversationId),
|
|
119
|
+
span_id: hexId(8),
|
|
120
|
+
parent_span_id: null,
|
|
121
|
+
timestamp: entry.timestamp,
|
|
122
|
+
sequence,
|
|
123
|
+
source: {
|
|
124
|
+
tool: entry.source || "unknown",
|
|
125
|
+
tool_version: null,
|
|
126
|
+
agent_role: agentRole,
|
|
127
|
+
collector: COLLECTOR_NAME,
|
|
128
|
+
collector_version: COLLECTOR_VERSION,
|
|
129
|
+
},
|
|
130
|
+
gen_ai: {
|
|
131
|
+
system: ci.provider,
|
|
132
|
+
request: {
|
|
133
|
+
model: ci.model,
|
|
134
|
+
max_tokens: entry.rawBody?.max_tokens ?? null,
|
|
135
|
+
temperature: entry.rawBody?.temperature ?? null,
|
|
136
|
+
top_p: entry.rawBody?.top_p ?? null,
|
|
137
|
+
stop_sequences: entry.rawBody?.stop_sequences || [],
|
|
138
|
+
},
|
|
139
|
+
response: {
|
|
140
|
+
model: usage.model,
|
|
141
|
+
finish_reasons: usage.finishReasons,
|
|
142
|
+
},
|
|
143
|
+
usage: {
|
|
144
|
+
input_tokens: usage.inputTokens || ci.totalTokens,
|
|
145
|
+
output_tokens: usage.outputTokens,
|
|
146
|
+
total_tokens: (usage.inputTokens || ci.totalTokens) + usage.outputTokens,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
usage_ext: {
|
|
150
|
+
cache_read_tokens: usage.cacheReadTokens,
|
|
151
|
+
cache_write_tokens: usage.cacheWriteTokens,
|
|
152
|
+
cost_usd: entry.costUsd,
|
|
153
|
+
},
|
|
154
|
+
http: {
|
|
155
|
+
method: "POST",
|
|
156
|
+
url: entry.targetUrl,
|
|
157
|
+
status_code: entry.httpStatus,
|
|
158
|
+
api_format: ci.apiFormat,
|
|
159
|
+
stream: usage.stream,
|
|
160
|
+
request_headers: privacy === "minimal" ? {} : redactHeaders(entry.requestHeaders),
|
|
161
|
+
response_headers: privacy === "minimal" ? {} : redactHeaders(entry.responseHeaders),
|
|
162
|
+
},
|
|
163
|
+
timings,
|
|
164
|
+
transfer: {
|
|
165
|
+
request_bytes: entry.requestBytes,
|
|
166
|
+
response_bytes: entry.responseBytes,
|
|
167
|
+
compressed: false,
|
|
168
|
+
},
|
|
169
|
+
context_lens: {
|
|
170
|
+
window_size: entry.contextLimit,
|
|
171
|
+
utilization: entry.contextLimit > 0
|
|
172
|
+
? Math.round((ci.totalTokens / entry.contextLimit) * 1000) / 1000
|
|
173
|
+
: 0,
|
|
174
|
+
system_tokens: ci.systemTokens,
|
|
175
|
+
tools_tokens: ci.toolsTokens,
|
|
176
|
+
messages_tokens: ci.messagesTokens,
|
|
177
|
+
composition,
|
|
178
|
+
growth: {
|
|
179
|
+
tokens_added_this_turn: tokensAdded,
|
|
180
|
+
cumulative_tokens: ci.totalTokens,
|
|
181
|
+
compaction_detected: compactionDetected,
|
|
182
|
+
},
|
|
183
|
+
security: {
|
|
184
|
+
alerts: (entry.securityAlerts || []).map((a) => ({
|
|
185
|
+
message_index: a.messageIndex,
|
|
186
|
+
role: a.role,
|
|
187
|
+
tool_name: a.toolName,
|
|
188
|
+
severity: a.severity,
|
|
189
|
+
pattern: a.pattern,
|
|
190
|
+
match: a.match,
|
|
191
|
+
offset: a.offset,
|
|
192
|
+
length: a.length,
|
|
193
|
+
})),
|
|
194
|
+
summary: {
|
|
195
|
+
high: (entry.securityAlerts || []).filter((a) => a.severity === "high").length,
|
|
196
|
+
medium: (entry.securityAlerts || []).filter((a) => a.severity === "medium").length,
|
|
197
|
+
info: (entry.securityAlerts || []).filter((a) => a.severity === "info").length,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
raw: {
|
|
202
|
+
request_body: privacy === "full" && entry.rawBody ? entry.rawBody : null,
|
|
203
|
+
response_body: privacy === "full" ? responseBodyForCapture(entry.response) : null,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function buildSessionLine(conversationId, conversation, model) {
|
|
208
|
+
return {
|
|
209
|
+
type: "session",
|
|
210
|
+
trace_id: traceIdFromConversation(conversationId),
|
|
211
|
+
started_at: conversation.firstSeen,
|
|
212
|
+
tool: conversation.source,
|
|
213
|
+
model,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=record.js.map
|