@mtharrison/loupe 1.1.1 → 1.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 +155 -41
- package/assets/screenshot1.png +0 -0
- package/assets/screenshot2.png +0 -0
- package/dist/client/app.css +365 -263
- package/dist/client/app.js +815 -658
- package/dist/index.d.ts +7 -8
- package/dist/index.js +392 -49
- package/dist/server.d.ts +1 -0
- package/dist/server.js +42 -11
- package/dist/session-nav.d.ts +10 -0
- package/dist/session-nav.js +91 -0
- package/dist/store.d.ts +6 -7
- package/dist/store.js +203 -45
- package/dist/types.d.ts +62 -9
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +14 -0
- package/examples/nested-tool-call.js +234 -0
- package/examples/openai-multiturn-tools.js +399 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -20,6 +20,16 @@ Most tracing tools assume hosted infrastructure, persistent storage, or producti
|
|
|
20
20
|
- cost rollups when token usage and pricing are available
|
|
21
21
|
- zero external services
|
|
22
22
|
|
|
23
|
+
## Screenshots
|
|
24
|
+
|
|
25
|
+
Conversation view with tool calls, staged traces, and session navigation:
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Request view showing the captured OpenAI payload for a multi-turn tool call:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
23
33
|
## Installation
|
|
24
34
|
|
|
25
35
|
```sh
|
|
@@ -38,15 +48,115 @@ Enable tracing:
|
|
|
38
48
|
export LLM_TRACE_ENABLED=1
|
|
39
49
|
```
|
|
40
50
|
|
|
51
|
+
If your app already uses a higher-level model interface or the official OpenAI client, Loupe can wrap that directly instead of requiring manual `record*` calls.
|
|
52
|
+
|
|
53
|
+
### `wrapOpenAIClient(client, getContext, config?)`
|
|
54
|
+
|
|
55
|
+
Wraps `client.chat.completions.create(...)` on an OpenAI-compatible client and records either an `invoke` trace or a `stream` trace based on `params.stream`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import {
|
|
59
|
+
wrapOpenAIClient,
|
|
60
|
+
} from '@mtharrison/loupe';
|
|
61
|
+
import OpenAI from 'openai';
|
|
62
|
+
|
|
63
|
+
const client = wrapOpenAIClient(
|
|
64
|
+
new OpenAI(),
|
|
65
|
+
() => ({
|
|
66
|
+
sessionId: 'session-123',
|
|
67
|
+
rootActorId: 'support-assistant',
|
|
68
|
+
actorId: 'support-assistant',
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const completion = await client.chat.completions.create({
|
|
73
|
+
model: 'gpt-4.1',
|
|
74
|
+
messages: [{ role: 'user', content: 'Summarize the latest notes.' }],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const stream = await client.chat.completions.create({
|
|
78
|
+
model: 'gpt-4.1',
|
|
79
|
+
messages: [{ role: 'user', content: 'Stream the same summary.' }],
|
|
80
|
+
stream: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
for await (const chunk of stream) {
|
|
84
|
+
process.stdout.write(chunk.choices?.[0]?.delta?.content || '');
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If you do not call `startServer()` yourself, the dashboard starts lazily on the first recorded trace.
|
|
89
|
+
|
|
90
|
+
When the server starts, Loupe prints the local URL:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
[llm-trace] dashboard: http://127.0.0.1:4319
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If `4319` is already in use and you did not explicitly configure a port, Loupe falls back to another free local port and prints that URL instead.
|
|
97
|
+
|
|
98
|
+
`wrapOpenAIClient()` is structurally typed, so Loupe's runtime API does not require the OpenAI SDK for normal library usage. The repo includes `openai` as a dev dependency for the bundled demo; if your own app instantiates `new OpenAI()` or runs the published example from a consumer install, install `openai` there too.
|
|
99
|
+
|
|
100
|
+
### `wrapChatModel(model, getContext, config?)`
|
|
101
|
+
|
|
102
|
+
Wraps any object with `invoke()` and `stream()` methods.
|
|
103
|
+
|
|
104
|
+
### Runnable OpenAI Tools Demo
|
|
105
|
+
|
|
106
|
+
There is also a runnable example at `examples/openai-multiturn-tools.js` that:
|
|
107
|
+
|
|
108
|
+
- starts the Loupe dashboard eagerly
|
|
109
|
+
- wraps an OpenAI client with `wrapOpenAIClient()`
|
|
110
|
+
- runs a multi-turn conversation with tool calls
|
|
111
|
+
- keeps the process alive so the in-memory traces stay visible in the dashboard
|
|
112
|
+
|
|
113
|
+
From this repo, after installing this package's dev dependencies, run:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install
|
|
117
|
+
export OPENAI_API_KEY=your-key
|
|
118
|
+
export LLM_TRACE_ENABLED=1
|
|
119
|
+
node examples/openai-multiturn-tools.js
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If you copy this example pattern into another app, install `openai` in that app before using `new OpenAI()`.
|
|
123
|
+
|
|
124
|
+
Supported demo environment variables: `OPENAI_MODEL`, `LLM_TRACE_PORT`, `LOUPE_OPEN_BROWSER`.
|
|
125
|
+
|
|
126
|
+
The script tries to open the dashboard automatically and prints the local URL either way. Set `LOUPE_OPEN_BROWSER=0` if you want to suppress the browser launch.
|
|
127
|
+
|
|
128
|
+
### Runnable Nested Tool-Call Demo
|
|
129
|
+
|
|
130
|
+
`examples/nested-tool-call.js` is a credential-free demo that:
|
|
131
|
+
|
|
132
|
+
- starts the Loupe dashboard eagerly
|
|
133
|
+
- wraps a root assistant model and a nested tool model
|
|
134
|
+
- invokes the nested tool model from inside the parent model call
|
|
135
|
+
- shows parent/child spans linked on the same trace
|
|
136
|
+
|
|
137
|
+
Run it with:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm install
|
|
141
|
+
export LLM_TRACE_ENABLED=1
|
|
142
|
+
node examples/nested-tool-call.js
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Low-Level Lifecycle API
|
|
146
|
+
|
|
147
|
+
If you need full control over trace boundaries, Loupe exposes a lower-level span lifecycle API modeled on OpenTelemetry concepts: start a span, add events, end it, and record exceptions.
|
|
148
|
+
|
|
149
|
+
Loupe stores GenAI span attributes using the OpenTelemetry semantic convention names where they apply, including `gen_ai.request.model`, `gen_ai.response.model`, `gen_ai.system`, `gen_ai.provider.name`, `gen_ai.operation.name`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and `gen_ai.conversation.id`.
|
|
150
|
+
|
|
41
151
|
Start the dashboard during app startup, then instrument a model call:
|
|
42
152
|
|
|
43
153
|
```ts
|
|
44
154
|
import {
|
|
45
155
|
getLocalLLMTracer,
|
|
46
156
|
isTraceEnabled,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
157
|
+
endSpan,
|
|
158
|
+
recordException,
|
|
159
|
+
startSpan,
|
|
50
160
|
type TraceContext,
|
|
51
161
|
} from '@mtharrison/loupe';
|
|
52
162
|
|
|
@@ -75,59 +185,63 @@ const request = {
|
|
|
75
185
|
options: {},
|
|
76
186
|
};
|
|
77
187
|
|
|
78
|
-
const
|
|
188
|
+
const spanId = startSpan(context, {
|
|
189
|
+
mode: 'invoke',
|
|
190
|
+
name: 'openai.chat.completions',
|
|
191
|
+
request,
|
|
192
|
+
});
|
|
79
193
|
|
|
80
194
|
try {
|
|
81
195
|
const response = await model.invoke(request.input, request.options);
|
|
82
|
-
|
|
196
|
+
endSpan(spanId, response);
|
|
83
197
|
return response;
|
|
84
198
|
} catch (error) {
|
|
85
|
-
|
|
199
|
+
recordException(spanId, error);
|
|
86
200
|
throw error;
|
|
87
201
|
}
|
|
88
202
|
```
|
|
89
203
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
When the server starts, Loupe prints the local URL:
|
|
93
|
-
|
|
94
|
-
```text
|
|
95
|
-
[llm-trace] dashboard: http://127.0.0.1:4319
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Streaming
|
|
204
|
+
### Streaming
|
|
99
205
|
|
|
100
|
-
Streaming works the same way. Loupe records each
|
|
206
|
+
Streaming works the same way. Loupe records each span event, first-chunk latency, and the reconstructed final response.
|
|
101
207
|
|
|
102
208
|
```ts
|
|
103
209
|
import {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
210
|
+
addSpanEvent,
|
|
211
|
+
endSpan,
|
|
212
|
+
recordException,
|
|
213
|
+
startSpan,
|
|
108
214
|
} from '@mtharrison/loupe';
|
|
109
215
|
|
|
110
|
-
const
|
|
216
|
+
const spanId = startSpan(context, {
|
|
217
|
+
mode: 'stream',
|
|
218
|
+
name: 'openai.chat.completions',
|
|
219
|
+
request,
|
|
220
|
+
});
|
|
111
221
|
|
|
112
222
|
try {
|
|
113
223
|
for await (const chunk of model.stream(request.input, request.options)) {
|
|
114
224
|
if (chunk?.type === 'finish') {
|
|
115
|
-
|
|
225
|
+
endSpan(spanId, chunk);
|
|
116
226
|
} else {
|
|
117
|
-
|
|
227
|
+
addSpanEvent(spanId, {
|
|
228
|
+
name: `stream.${chunk?.type || 'event'}`,
|
|
229
|
+
attributes: chunk,
|
|
230
|
+
payload: chunk,
|
|
231
|
+
});
|
|
118
232
|
}
|
|
119
233
|
|
|
120
234
|
yield chunk;
|
|
121
235
|
}
|
|
122
236
|
} catch (error) {
|
|
123
|
-
|
|
237
|
+
recordException(spanId, error);
|
|
124
238
|
throw error;
|
|
125
239
|
}
|
|
126
240
|
```
|
|
127
241
|
|
|
128
242
|
## Trace Context
|
|
129
243
|
|
|
130
|
-
Loupe gets its hierarchy and filters from the context you pass to `
|
|
244
|
+
Loupe gets its hierarchy and filters from the context you pass to `startSpan()`.
|
|
131
245
|
|
|
132
246
|
### Generic context fields
|
|
133
247
|
|
|
@@ -213,7 +327,7 @@ If usage or pricing is missing, Loupe still records the trace, but cost will sho
|
|
|
213
327
|
|
|
214
328
|
The local dashboard includes:
|
|
215
329
|
|
|
216
|
-
-
|
|
330
|
+
- session-first tree navigation
|
|
217
331
|
- hierarchy-aware browsing
|
|
218
332
|
- conversation, request, response, context, and stream views
|
|
219
333
|
- formatted and raw JSON modes
|
|
@@ -231,7 +345,7 @@ Environment variables:
|
|
|
231
345
|
| --- | --- | --- |
|
|
232
346
|
| `LLM_TRACE_ENABLED` | `false` | Enables Loupe. |
|
|
233
347
|
| `LLM_TRACE_HOST` | `127.0.0.1` | Host for the local dashboard server. |
|
|
234
|
-
| `LLM_TRACE_PORT` | `4319` | Port for the local dashboard server. |
|
|
348
|
+
| `LLM_TRACE_PORT` | `4319` | Port for the local dashboard server. If unset, Loupe tries `4319` first and falls back to a free local port if it is already in use. |
|
|
235
349
|
| `LLM_TRACE_MAX_TRACES` | `1000` | Maximum number of traces kept in memory. |
|
|
236
350
|
| `LLM_TRACE_UI_HOT_RELOAD` | auto in local interactive dev | Enables UI rebuild + reload while developing the dashboard itself. |
|
|
237
351
|
|
|
@@ -239,7 +353,7 @@ Programmatic configuration is also available through `getLocalLLMTracer(config)`
|
|
|
239
353
|
|
|
240
354
|
## API
|
|
241
355
|
|
|
242
|
-
|
|
356
|
+
Loupe exposes both low-level span lifecycle functions and lightweight wrappers.
|
|
243
357
|
|
|
244
358
|
### `isTraceEnabled()`
|
|
245
359
|
|
|
@@ -257,31 +371,31 @@ Returns the singleton tracer instance. This is useful if you want to:
|
|
|
257
371
|
|
|
258
372
|
Starts the local dashboard server eagerly instead of waiting for the first trace.
|
|
259
373
|
|
|
260
|
-
### `
|
|
374
|
+
### `startSpan(context, options?, config?)`
|
|
261
375
|
|
|
262
|
-
Creates
|
|
376
|
+
Creates a span and returns its Loupe `spanId`. Pass `mode`, `name`, and `request` in `options` to describe the operation. Nested spans are linked automatically when wrapped calls invoke other wrapped calls in the same async flow.
|
|
263
377
|
|
|
264
|
-
### `
|
|
378
|
+
### `addSpanEvent(spanId, event, config?)`
|
|
265
379
|
|
|
266
|
-
|
|
380
|
+
Appends an event to an existing span. For streaming traces, pass the raw chunk as `event.payload` to preserve chunk reconstruction in the UI.
|
|
267
381
|
|
|
268
|
-
### `
|
|
382
|
+
### `endSpan(spanId, response, config?)`
|
|
269
383
|
|
|
270
|
-
|
|
384
|
+
Marks a span as complete and stores the final response payload.
|
|
271
385
|
|
|
272
|
-
### `
|
|
386
|
+
### `recordException(spanId, error, config?)`
|
|
273
387
|
|
|
274
|
-
|
|
388
|
+
Marks a span as failed and stores a serialized exception payload.
|
|
275
389
|
|
|
276
|
-
|
|
390
|
+
All of these functions forward to the singleton tracer returned by `getLocalLLMTracer()`.
|
|
277
391
|
|
|
278
|
-
|
|
392
|
+
### `wrapChatModel(model, getContext, config?)`
|
|
279
393
|
|
|
280
|
-
|
|
394
|
+
Returns a traced model wrapper for `invoke()` and `stream()`.
|
|
281
395
|
|
|
282
|
-
|
|
396
|
+
### `wrapOpenAIClient(client, getContext, config?)`
|
|
283
397
|
|
|
284
|
-
|
|
398
|
+
Returns a traced OpenAI client wrapper for `chat.completions.create(...)`.
|
|
285
399
|
|
|
286
400
|
## HTTP Endpoints
|
|
287
401
|
|
|
Binary file
|
|
Binary file
|