@mtharrison/loupe 1.0.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.
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildUIAssets = buildUIAssets;
7
+ exports.maybeStartUIWatcher = maybeStartUIWatcher;
8
+ const esbuild_1 = require("esbuild");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const utils_1 = require("./utils");
11
+ const rootDir = node_path_1.default.resolve(__dirname, '..');
12
+ const outdir = node_path_1.default.join(__dirname, 'client');
13
+ const entryPoint = node_path_1.default.join(rootDir, 'client', 'main.tsx');
14
+ function createBuildOptions() {
15
+ return {
16
+ entryNames: 'app',
17
+ entryPoints: [entryPoint],
18
+ bundle: true,
19
+ outdir,
20
+ format: 'esm',
21
+ platform: 'browser',
22
+ target: ['es2022'],
23
+ jsx: 'automatic',
24
+ loader: {
25
+ '.svg': 'dataurl',
26
+ },
27
+ splitting: false,
28
+ minify: false,
29
+ sourcemap: false,
30
+ logLevel: 'silent',
31
+ };
32
+ }
33
+ async function buildUIAssets() {
34
+ await (0, esbuild_1.build)(createBuildOptions());
35
+ }
36
+ async function maybeStartUIWatcher(onReload, enabled) {
37
+ const watchEnabled = typeof enabled === 'boolean'
38
+ ? enabled
39
+ : process.env.LLM_TRACE_UI_HOT_RELOAD
40
+ ? (0, utils_1.envFlag)('LLM_TRACE_UI_HOT_RELOAD')
41
+ : !process.env.CI && !!process.stdout.isTTY && process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
42
+ if (!watchEnabled) {
43
+ return null;
44
+ }
45
+ const buildContext = await (0, esbuild_1.context)({
46
+ ...createBuildOptions(),
47
+ plugins: [
48
+ {
49
+ name: 'llm-trace-hot-reload',
50
+ setup(build) {
51
+ build.onEnd((result) => {
52
+ if (!result.errors.length) {
53
+ onReload();
54
+ }
55
+ });
56
+ },
57
+ },
58
+ ],
59
+ });
60
+ await buildContext.watch();
61
+ return {
62
+ async stop() {
63
+ await buildContext.dispose();
64
+ },
65
+ };
66
+ }
package/dist/ui.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function renderAppHtml(): string;
package/dist/ui.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderAppHtml = renderAppHtml;
4
+ const brand_1 = require("./brand");
5
+ const theme_1 = require("./theme");
6
+ function renderAppHtml() {
7
+ const brandIcon = (0, brand_1.renderBrandMarkSvg)();
8
+ const faviconHref = `data:image/svg+xml,${encodeURIComponent(brandIcon)}`;
9
+ const themeBootstrapScript = (0, theme_1.renderThemeBootstrapScript)();
10
+ return `<!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>${brand_1.BRAND_NAME}</title>
16
+ <link rel="icon" href="${faviconHref}" />
17
+ <script>${themeBootstrapScript}</script>
18
+ <link rel="stylesheet" href="/assets/app.css" />
19
+ </head>
20
+ <body>
21
+ <div id="app"></div>
22
+ <script type="module" src="/assets/app.js"></script>
23
+ </body>
24
+ </html>`;
25
+ }
@@ -0,0 +1,10 @@
1
+ import { type NormalizedTraceContext, type TraceContext, type TraceMode, type TraceRecord, type TraceSummary } from './types';
2
+ export declare function safeClone<T>(value: T): T;
3
+ export declare function toErrorPayload(error: any): Record<string, any> | null;
4
+ export declare function sanitizeHeaders(headers: Record<string, any> | undefined): Record<string, any>;
5
+ export declare function envFlag(name: string): boolean;
6
+ export declare function stringifyRecord(record: Record<string, any>): Record<string, string>;
7
+ export declare function normalizeTraceContext(context: TraceContext | undefined, mode: TraceMode): NormalizedTraceContext;
8
+ export declare function summariseValue(value: unknown, maxLength?: number): string;
9
+ export declare function getUsageCostUsd(usage: Record<string, any> | null | undefined): number | null;
10
+ export declare function toSummary(trace: TraceRecord): TraceSummary;
package/dist/utils.js ADDED
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeClone = safeClone;
4
+ exports.toErrorPayload = toErrorPayload;
5
+ exports.sanitizeHeaders = sanitizeHeaders;
6
+ exports.envFlag = envFlag;
7
+ exports.stringifyRecord = stringifyRecord;
8
+ exports.normalizeTraceContext = normalizeTraceContext;
9
+ exports.summariseValue = summariseValue;
10
+ exports.getUsageCostUsd = getUsageCostUsd;
11
+ exports.toSummary = toSummary;
12
+ function safeClone(value) {
13
+ if (value === undefined) {
14
+ return value;
15
+ }
16
+ try {
17
+ const clone = globalThis.structuredClone;
18
+ if (typeof clone === 'function') {
19
+ return clone(value);
20
+ }
21
+ }
22
+ catch (_err) {
23
+ // fall through
24
+ }
25
+ try {
26
+ return JSON.parse(JSON.stringify(value));
27
+ }
28
+ catch (_err) {
29
+ return value;
30
+ }
31
+ }
32
+ function toErrorPayload(error) {
33
+ if (!error) {
34
+ return null;
35
+ }
36
+ const payload = {
37
+ message: error.message,
38
+ name: error.name,
39
+ };
40
+ for (const key of ['code', 'param', 'status', 'statusCode', 'type']) {
41
+ if (error[key] !== undefined) {
42
+ payload[key] = error[key];
43
+ }
44
+ }
45
+ if (error.stack) {
46
+ payload.stack = error.stack;
47
+ }
48
+ return payload;
49
+ }
50
+ function sanitizeHeaders(headers) {
51
+ if (!headers || typeof headers !== 'object') {
52
+ return {};
53
+ }
54
+ const sanitized = {};
55
+ for (const [key, value] of Object.entries(headers)) {
56
+ sanitized[key] = isSensitiveHeader(key) ? '[REDACTED]' : value;
57
+ }
58
+ return sanitized;
59
+ }
60
+ function isSensitiveHeader(key) {
61
+ return ['authorization', 'api-key', 'x-api-key', 'openai-api-key'].includes(String(key).toLowerCase());
62
+ }
63
+ function envFlag(name) {
64
+ const value = process.env[name];
65
+ if (!value) {
66
+ return false;
67
+ }
68
+ return !['0', 'false', 'off', 'no'].includes(String(value).toLowerCase());
69
+ }
70
+ function stringifyRecord(record) {
71
+ return Object.entries(record).reduce((acc, [key, value]) => {
72
+ if (value !== undefined && value !== null && value !== '') {
73
+ acc[key] = String(value);
74
+ }
75
+ return acc;
76
+ }, {});
77
+ }
78
+ function normalizeKind(value) {
79
+ if (!value) {
80
+ return null;
81
+ }
82
+ switch (value) {
83
+ case 'agent':
84
+ return 'actor';
85
+ case 'delegated-agent':
86
+ return 'child-actor';
87
+ case 'workflow-state':
88
+ return 'stage';
89
+ case 'watchdog':
90
+ return 'guardrail';
91
+ default:
92
+ return value;
93
+ }
94
+ }
95
+ function deriveGuardrailPhase(value) {
96
+ if (!value) {
97
+ return null;
98
+ }
99
+ const normalized = String(value).toLowerCase();
100
+ if (normalized.startsWith('input')) {
101
+ return 'input';
102
+ }
103
+ if (normalized.startsWith('output')) {
104
+ return 'output';
105
+ }
106
+ return null;
107
+ }
108
+ function normalizeTraceContext(context, mode) {
109
+ const raw = context || {};
110
+ const sessionId = raw.sessionId || raw.chatId || raw.rootSessionId || raw.rootChatId || null;
111
+ const rootSessionId = raw.rootSessionId || raw.rootChatId || sessionId || null;
112
+ const rootActorId = raw.rootActorId || raw.topLevelAgentId || raw.actorId || raw.agentId || null;
113
+ const actorId = raw.actorId || raw.agentId || rootActorId || null;
114
+ const actorType = raw.actorType || raw.contextType || null;
115
+ const stage = raw.stage || raw.workflowState || null;
116
+ const guardrailType = raw.guardrailType || raw.systemType || null;
117
+ const guardrailPhase = raw.guardrailPhase || raw.watchdogPhase || deriveGuardrailPhase(guardrailType);
118
+ const isChildActor = !!(actorId && rootActorId && actorId !== rootActorId);
119
+ const explicitKind = normalizeKind(raw.kind);
120
+ let kind = explicitKind || 'actor';
121
+ if (!explicitKind) {
122
+ if (guardrailType || guardrailPhase) {
123
+ kind = 'guardrail';
124
+ }
125
+ else if (stage) {
126
+ kind = 'stage';
127
+ }
128
+ else if (isChildActor) {
129
+ kind = 'child-actor';
130
+ }
131
+ }
132
+ const provider = raw.provider || null;
133
+ const model = raw.model || null;
134
+ const tags = stringifyRecord({
135
+ ...(raw.tags || {}),
136
+ actorId,
137
+ actorType,
138
+ guardrailPhase,
139
+ guardrailType,
140
+ kind,
141
+ mode,
142
+ model,
143
+ parentSessionId: raw.parentSessionId || raw.parentChatId || null,
144
+ provider,
145
+ rootActorId,
146
+ rootSessionId,
147
+ sessionId,
148
+ stage,
149
+ tenantId: raw.tenantId || null,
150
+ userId: raw.userId || null,
151
+ agentId: actorId,
152
+ chatId: sessionId,
153
+ contextType: actorType,
154
+ parentChatId: raw.parentSessionId || raw.parentChatId || null,
155
+ rootChatId: rootSessionId,
156
+ systemType: guardrailType,
157
+ topLevelAgentId: rootActorId,
158
+ watchdogPhase: guardrailPhase,
159
+ workflowState: stage,
160
+ });
161
+ return {
162
+ actorId,
163
+ actorType,
164
+ guardrailPhase,
165
+ guardrailType,
166
+ kind,
167
+ model,
168
+ parentSessionId: raw.parentSessionId || raw.parentChatId || null,
169
+ provider,
170
+ rootActorId,
171
+ rootSessionId,
172
+ sessionId,
173
+ stage,
174
+ tags,
175
+ tenantId: raw.tenantId || null,
176
+ userId: raw.userId || null,
177
+ agentId: actorId,
178
+ chatId: sessionId,
179
+ contextType: actorType,
180
+ parentChatId: raw.parentSessionId || raw.parentChatId || null,
181
+ rootChatId: rootSessionId,
182
+ systemType: guardrailType,
183
+ topLevelAgentId: rootActorId,
184
+ watchdogPhase: guardrailPhase,
185
+ workflowState: stage,
186
+ hierarchy: {
187
+ childActorId: isChildActor ? actorId : null,
188
+ guardrailPhase,
189
+ guardrailType,
190
+ kind,
191
+ rootActorId: rootActorId || 'unknown-actor',
192
+ sessionId: rootSessionId || sessionId || 'unknown-session',
193
+ stage,
194
+ chatId: rootSessionId || sessionId || 'unknown-session',
195
+ delegatedAgentId: isChildActor ? actorId : null,
196
+ systemType: guardrailType,
197
+ topLevelAgentId: rootActorId || 'unknown-actor',
198
+ watchdogPhase: guardrailPhase,
199
+ workflowState: stage,
200
+ },
201
+ };
202
+ }
203
+ function summariseValue(value, maxLength = 160) {
204
+ if (value === null || value === undefined) {
205
+ return '';
206
+ }
207
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
208
+ if (text.length <= maxLength) {
209
+ return text;
210
+ }
211
+ return `${text.slice(0, maxLength - 1)}...`;
212
+ }
213
+ function getUsageCostUsd(usage) {
214
+ const promptTokens = toFiniteNumber(usage?.tokens?.prompt);
215
+ const completionTokens = toFiniteNumber(usage?.tokens?.completion);
216
+ const promptPricing = toFiniteNumber(usage?.pricing?.prompt);
217
+ const completionPricing = toFiniteNumber(usage?.pricing?.completion);
218
+ if (promptTokens === null || completionTokens === null || promptPricing === null || completionPricing === null) {
219
+ return null;
220
+ }
221
+ return roundCostUsd(promptTokens * promptPricing + completionTokens * completionPricing);
222
+ }
223
+ function toFiniteNumber(value) {
224
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
225
+ }
226
+ function roundCostUsd(value) {
227
+ return Math.round(value * 1e12) / 1e12;
228
+ }
229
+ function extractRequestPreview(request) {
230
+ const messages = request?.input?.messages;
231
+ if (!Array.isArray(messages) || messages.length === 0) {
232
+ return '';
233
+ }
234
+ const lastUserMessage = [...messages].reverse().find((message) => message?.role === 'user');
235
+ if (!lastUserMessage) {
236
+ return summariseValue(messages[messages.length - 1]?.content);
237
+ }
238
+ return summariseValue(lastUserMessage.content);
239
+ }
240
+ function extractResponsePreview(trace) {
241
+ if (trace.mode === 'stream') {
242
+ const content = trace.stream?.reconstructed?.message?.content;
243
+ if (content) {
244
+ return summariseValue(content);
245
+ }
246
+ }
247
+ const content = trace.response?.message?.content;
248
+ if (content) {
249
+ return summariseValue(content);
250
+ }
251
+ if (trace.error?.message) {
252
+ return trace.error.message;
253
+ }
254
+ return '';
255
+ }
256
+ function toSummary(trace) {
257
+ const durationMs = trace.endedAt ? Math.max(0, Date.parse(trace.endedAt) - Date.parse(trace.startedAt)) : null;
258
+ return {
259
+ costUsd: getUsageCostUsd(trace.usage),
260
+ durationMs,
261
+ endedAt: trace.endedAt,
262
+ hierarchy: safeClone(trace.hierarchy),
263
+ id: trace.id,
264
+ kind: trace.kind,
265
+ mode: trace.mode,
266
+ model: trace.model,
267
+ provider: trace.provider,
268
+ requestPreview: extractRequestPreview(trace.request),
269
+ responsePreview: extractResponsePreview(trace),
270
+ startedAt: trace.startedAt,
271
+ status: trace.status,
272
+ stream: trace.stream
273
+ ? {
274
+ chunkCount: trace.stream.chunkCount,
275
+ firstChunkMs: trace.stream.firstChunkMs,
276
+ }
277
+ : null,
278
+ tags: safeClone(trace.tags),
279
+ };
280
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@mtharrison/loupe",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight local tracing dashboard for LLM calls",
5
+ "author": "Matt Harrison",
6
+ "license": "MIT",
7
+ "type": "commonjs",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "require": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "assets",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "sideEffects": false,
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json && node scripts/build-ui.mjs",
33
+ "release": "npx semantic-release",
34
+ "test": "npm run build && node --test"
35
+ },
36
+ "release": {
37
+ "branches": [
38
+ "main"
39
+ ],
40
+ "plugins": [
41
+ "@semantic-release/commit-analyzer",
42
+ "@semantic-release/release-notes-generator",
43
+ "@semantic-release/npm",
44
+ "@semantic-release/github"
45
+ ]
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.0.0",
49
+ "@types/react": "^19.2.14",
50
+ "@types/react-dom": "^19.2.3",
51
+ "typescript": "^5.9.2"
52
+ },
53
+ "keywords": [
54
+ "llm",
55
+ "observability",
56
+ "openai",
57
+ "tracing",
58
+ "trace",
59
+ "dashboard",
60
+ "local-dev"
61
+ ],
62
+ "dependencies": {
63
+ "clsx": "^2.1.1",
64
+ "esbuild": "^0.27.4",
65
+ "lucide-react": "^0.577.0",
66
+ "react": "^19.2.4",
67
+ "react-dom": "^19.2.4",
68
+ "react-markdown": "^10.1.0",
69
+ "remark-gfm": "^4.0.1"
70
+ }
71
+ }