@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/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ChatModelLike, type LocalLLMTracer, type
|
|
2
|
-
export type { ChatModelLike, HierarchyNode, HierarchyResponse, LocalLLMTracer, NormalizedTraceContext, TraceConfig, TraceContext, TraceEvent, TraceFilters, TraceHierarchy, TraceListResponse, TraceMode, TraceRecord, TraceRequest, TraceServer, TraceStatus, TraceSummary, TraceTags, UIReloadEvent, } from './types';
|
|
1
|
+
import { type ChatModelLike, type LocalLLMTracer, type OpenAIChatCompletionCreateParamsLike, type OpenAIClientLike, type SpanEventInput, type SpanStartOptions, type TraceConfig, type TraceContext } from './types';
|
|
2
|
+
export type { ChatModelLike, HierarchyNode, HierarchyResponse, LocalLLMTracer, NormalizedTraceContext, OpenAIChatCompletionCreateParamsLike, OpenAIChatCompletionStreamLike, OpenAIClientLike, SpanAttributes, SpanContext, SpanEvent, SpanEventInput, SpanKind, SpanStartOptions, SpanStatus, SpanStatusCode, TraceConfig, TraceContext, TraceEvent, TraceFilters, TraceHierarchy, TraceListResponse, TraceMode, TraceRecord, TraceRequest, TraceServer, TraceStatus, TraceSummary, TraceTags, UIReloadEvent, } from './types';
|
|
3
3
|
export declare function isTraceEnabled(): boolean;
|
|
4
4
|
export declare function getLocalLLMTracer(config?: TraceConfig): LocalLLMTracer;
|
|
5
5
|
export declare function startTraceServer(config?: TraceConfig): Promise<{
|
|
@@ -7,11 +7,10 @@ export declare function startTraceServer(config?: TraceConfig): Promise<{
|
|
|
7
7
|
port: number;
|
|
8
8
|
url: string;
|
|
9
9
|
}>;
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function recordStreamFinish(traceId: string, chunk: unknown, config?: TraceConfig): void;
|
|
15
|
-
export declare function recordError(traceId: string, error: unknown, config?: TraceConfig): void;
|
|
10
|
+
export declare function startSpan(context: TraceContext, options?: SpanStartOptions, config?: TraceConfig): string;
|
|
11
|
+
export declare function endSpan(spanId: string, response: unknown, config?: TraceConfig): void;
|
|
12
|
+
export declare function addSpanEvent(spanId: string, event: SpanEventInput, config?: TraceConfig): void;
|
|
13
|
+
export declare function recordException(spanId: string, error: unknown, config?: TraceConfig): void;
|
|
16
14
|
export declare function __resetLocalLLMTracerForTests(): void;
|
|
17
15
|
export declare function wrapChatModel<TModel extends ChatModelLike<TInput, TOptions, TValue, TChunk>, TInput = any, TOptions = any, TValue = any, TChunk = any>(model: TModel, getContext: () => TraceContext, config?: TraceConfig): TModel;
|
|
16
|
+
export declare function wrapOpenAIClient<TClient extends OpenAIClientLike<TParams, TOptions, TResponse, TChunk>, TParams extends OpenAIChatCompletionCreateParamsLike = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any>(client: TClient, getContext: () => TraceContext, config?: TraceConfig): TClient;
|
package/dist/index.js
CHANGED
|
@@ -3,19 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isTraceEnabled = isTraceEnabled;
|
|
4
4
|
exports.getLocalLLMTracer = getLocalLLMTracer;
|
|
5
5
|
exports.startTraceServer = startTraceServer;
|
|
6
|
-
exports.
|
|
7
|
-
exports.
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
10
|
-
exports.recordStreamFinish = recordStreamFinish;
|
|
11
|
-
exports.recordError = recordError;
|
|
6
|
+
exports.startSpan = startSpan;
|
|
7
|
+
exports.endSpan = endSpan;
|
|
8
|
+
exports.addSpanEvent = addSpanEvent;
|
|
9
|
+
exports.recordException = recordException;
|
|
12
10
|
exports.__resetLocalLLMTracerForTests = __resetLocalLLMTracerForTests;
|
|
13
11
|
exports.wrapChatModel = wrapChatModel;
|
|
12
|
+
exports.wrapOpenAIClient = wrapOpenAIClient;
|
|
13
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
14
14
|
const server_1 = require("./server");
|
|
15
15
|
const store_1 = require("./store");
|
|
16
16
|
const ui_build_1 = require("./ui-build");
|
|
17
17
|
const utils_1 = require("./utils");
|
|
18
18
|
let singleton = null;
|
|
19
|
+
const DEFAULT_TRACE_PORT = 4319;
|
|
20
|
+
const activeSpanStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
19
21
|
function isTraceEnabled() {
|
|
20
22
|
return (0, utils_1.envFlag)('LLM_TRACE_ENABLED');
|
|
21
23
|
}
|
|
@@ -31,23 +33,17 @@ function getLocalLLMTracer(config = {}) {
|
|
|
31
33
|
function startTraceServer(config = {}) {
|
|
32
34
|
return getLocalLLMTracer(config).startServer();
|
|
33
35
|
}
|
|
34
|
-
function
|
|
35
|
-
return getLocalLLMTracer(config).
|
|
36
|
+
function startSpan(context, options = {}, config = {}) {
|
|
37
|
+
return getLocalLLMTracer(config).startSpan(context, options);
|
|
36
38
|
}
|
|
37
|
-
function
|
|
38
|
-
getLocalLLMTracer(config).
|
|
39
|
+
function endSpan(spanId, response, config = {}) {
|
|
40
|
+
getLocalLLMTracer(config).endSpan(spanId, response);
|
|
39
41
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
+
function addSpanEvent(spanId, event, config = {}) {
|
|
43
|
+
getLocalLLMTracer(config).addSpanEvent(spanId, event);
|
|
42
44
|
}
|
|
43
|
-
function
|
|
44
|
-
getLocalLLMTracer(config).
|
|
45
|
-
}
|
|
46
|
-
function recordStreamFinish(traceId, chunk, config = {}) {
|
|
47
|
-
getLocalLLMTracer(config).recordStreamFinish(traceId, chunk);
|
|
48
|
-
}
|
|
49
|
-
function recordError(traceId, error, config = {}) {
|
|
50
|
-
getLocalLLMTracer(config).recordError(traceId, error);
|
|
45
|
+
function recordException(spanId, error, config = {}) {
|
|
46
|
+
getLocalLLMTracer(config).recordException(spanId, error);
|
|
51
47
|
}
|
|
52
48
|
function __resetLocalLLMTracerForTests() {
|
|
53
49
|
if (singleton?.uiWatcher) {
|
|
@@ -68,14 +64,19 @@ function wrapChatModel(model, getContext, config) {
|
|
|
68
64
|
if (!tracer.isEnabled()) {
|
|
69
65
|
return model.invoke(input, options);
|
|
70
66
|
}
|
|
71
|
-
const traceId = tracer.
|
|
67
|
+
const traceId = tracer.startSpan(getContext ? getContext() : {}, {
|
|
68
|
+
attributes: { 'gen_ai.operation.name': 'chat' },
|
|
69
|
+
mode: 'invoke',
|
|
70
|
+
name: 'llm.invoke',
|
|
71
|
+
request: { input: input, options: options },
|
|
72
|
+
});
|
|
72
73
|
try {
|
|
73
|
-
const response = await model.invoke(input, options);
|
|
74
|
-
tracer.
|
|
74
|
+
const response = await tracer.runWithActiveSpan(traceId, () => model.invoke(input, options));
|
|
75
|
+
tracer.endSpan(traceId, response);
|
|
75
76
|
return response;
|
|
76
77
|
}
|
|
77
78
|
catch (error) {
|
|
78
|
-
tracer.
|
|
79
|
+
tracer.recordException(traceId, error);
|
|
79
80
|
throw error;
|
|
80
81
|
}
|
|
81
82
|
},
|
|
@@ -85,29 +86,101 @@ function wrapChatModel(model, getContext, config) {
|
|
|
85
86
|
yield* model.stream(input, options);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
|
-
const traceId = tracer.
|
|
89
|
+
const traceId = tracer.startSpan(getContext ? getContext() : {}, {
|
|
90
|
+
attributes: { 'gen_ai.operation.name': 'chat' },
|
|
91
|
+
mode: 'stream',
|
|
92
|
+
name: 'llm.stream',
|
|
93
|
+
request: { input: input, options: options },
|
|
94
|
+
});
|
|
89
95
|
try {
|
|
90
|
-
const stream = model.stream(input, options);
|
|
96
|
+
const stream = tracer.runWithActiveSpan(traceId, () => model.stream(input, options));
|
|
91
97
|
for await (const chunk of stream) {
|
|
92
98
|
if (chunk?.type === 'finish') {
|
|
93
|
-
tracer.
|
|
99
|
+
tracer.endSpan(traceId, chunk);
|
|
94
100
|
}
|
|
95
101
|
else {
|
|
96
|
-
tracer.
|
|
102
|
+
tracer.addSpanEvent(traceId, (0, utils_1.toSpanEventInputFromChunk)(chunk));
|
|
97
103
|
}
|
|
98
104
|
yield chunk;
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
catch (error) {
|
|
102
|
-
tracer.
|
|
108
|
+
tracer.recordException(traceId, error);
|
|
103
109
|
throw error;
|
|
104
110
|
}
|
|
105
111
|
},
|
|
106
112
|
};
|
|
107
113
|
}
|
|
114
|
+
function wrapOpenAIClient(client, getContext, config) {
|
|
115
|
+
if (!client || typeof client.chat?.completions?.create !== 'function') {
|
|
116
|
+
throw new TypeError('wrapOpenAIClient expects an OpenAI client with chat.completions.create().');
|
|
117
|
+
}
|
|
118
|
+
const wrappedCompletions = new Proxy(client.chat.completions, {
|
|
119
|
+
get(target, prop, receiver) {
|
|
120
|
+
if (prop === 'create') {
|
|
121
|
+
return async (params, options) => {
|
|
122
|
+
const tracer = getLocalLLMTracer(config);
|
|
123
|
+
if (!tracer.isEnabled()) {
|
|
124
|
+
return target.create.call(target, params, options);
|
|
125
|
+
}
|
|
126
|
+
const context = withOpenAITraceContext(getContext ? getContext() : {}, params);
|
|
127
|
+
if (params?.stream) {
|
|
128
|
+
const traceId = tracer.startSpan(context, {
|
|
129
|
+
attributes: { 'gen_ai.operation.name': 'chat' },
|
|
130
|
+
mode: 'stream',
|
|
131
|
+
name: 'openai.chat.completions',
|
|
132
|
+
request: { input: params, options: options },
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const stream = await tracer.runWithActiveSpan(traceId, () => target.create.call(target, params, options));
|
|
136
|
+
return wrapOpenAIChatCompletionsStream(stream, tracer, traceId);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
tracer.recordException(traceId, error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const traceId = tracer.startSpan(context, {
|
|
144
|
+
attributes: { 'gen_ai.operation.name': 'chat' },
|
|
145
|
+
mode: 'invoke',
|
|
146
|
+
name: 'openai.chat.completions',
|
|
147
|
+
request: { input: params, options: options },
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
const response = await tracer.runWithActiveSpan(traceId, () => target.create.call(target, params, options));
|
|
151
|
+
tracer.endSpan(traceId, normalizeOpenAIChatCompletionResponse(response));
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
tracer.recordException(traceId, error);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const wrappedChat = new Proxy(client.chat, {
|
|
164
|
+
get(target, prop, receiver) {
|
|
165
|
+
if (prop === 'completions') {
|
|
166
|
+
return wrappedCompletions;
|
|
167
|
+
}
|
|
168
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return new Proxy(client, {
|
|
172
|
+
get(target, prop, receiver) {
|
|
173
|
+
if (prop === 'chat') {
|
|
174
|
+
return wrappedChat;
|
|
175
|
+
}
|
|
176
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
108
180
|
class LocalLLMTracerImpl {
|
|
109
181
|
config;
|
|
110
182
|
loggedUrl;
|
|
183
|
+
portWasExplicit;
|
|
111
184
|
server;
|
|
112
185
|
serverFailed;
|
|
113
186
|
serverInfo;
|
|
@@ -121,6 +194,7 @@ class LocalLLMTracerImpl {
|
|
|
121
194
|
port: 4319,
|
|
122
195
|
uiHotReload: false,
|
|
123
196
|
};
|
|
197
|
+
this.portWasExplicit = false;
|
|
124
198
|
this.configure(config);
|
|
125
199
|
this.store = new store_1.TraceStore({ maxTraces: this.config.maxTraces });
|
|
126
200
|
this.server = null;
|
|
@@ -134,9 +208,11 @@ class LocalLLMTracerImpl {
|
|
|
134
208
|
if (this.serverInfo && (config.host || config.port)) {
|
|
135
209
|
return;
|
|
136
210
|
}
|
|
211
|
+
const explicitPort = getConfiguredPort(config.port, process.env.LLM_TRACE_PORT, this.portWasExplicit ? this.config.port : undefined);
|
|
212
|
+
this.portWasExplicit = explicitPort !== undefined;
|
|
137
213
|
this.config = {
|
|
138
214
|
host: config.host || this.config.host || process.env.LLM_TRACE_HOST || '127.0.0.1',
|
|
139
|
-
port:
|
|
215
|
+
port: explicitPort ?? DEFAULT_TRACE_PORT,
|
|
140
216
|
maxTraces: Number(config.maxTraces || this.config.maxTraces || process.env.LLM_TRACE_MAX_TRACES) || 1000,
|
|
141
217
|
uiHotReload: typeof config.uiHotReload === 'boolean'
|
|
142
218
|
? config.uiHotReload
|
|
@@ -151,6 +227,27 @@ class LocalLLMTracerImpl {
|
|
|
151
227
|
isEnabled() {
|
|
152
228
|
return isTraceEnabled();
|
|
153
229
|
}
|
|
230
|
+
startSpan(context, options = {}) {
|
|
231
|
+
void this.startServer();
|
|
232
|
+
const parentSpanId = options.parentSpanId || activeSpanStorage.getStore() || null;
|
|
233
|
+
return this.store.startSpan(context, {
|
|
234
|
+
...options,
|
|
235
|
+
parentSpanId,
|
|
236
|
+
request: normaliseRequest(options.request || {}),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
runWithActiveSpan(spanId, callback) {
|
|
240
|
+
return activeSpanStorage.run(spanId, callback);
|
|
241
|
+
}
|
|
242
|
+
addSpanEvent(spanId, event) {
|
|
243
|
+
this.store.addSpanEvent(spanId, (0, utils_1.safeClone)(event));
|
|
244
|
+
}
|
|
245
|
+
endSpan(spanId, response) {
|
|
246
|
+
this.store.endSpan(spanId, (0, utils_1.safeClone)(response));
|
|
247
|
+
}
|
|
248
|
+
recordException(spanId, error) {
|
|
249
|
+
this.store.recordException(spanId, error);
|
|
250
|
+
}
|
|
154
251
|
startServer() {
|
|
155
252
|
if (!this.isEnabled() || this.serverFailed) {
|
|
156
253
|
return Promise.resolve(this.serverInfo);
|
|
@@ -163,13 +260,16 @@ class LocalLLMTracerImpl {
|
|
|
163
260
|
}
|
|
164
261
|
this.serverStartPromise = (async () => {
|
|
165
262
|
try {
|
|
166
|
-
this.server = (0, server_1.createTraceServer)(this.store,
|
|
263
|
+
this.server = (0, server_1.createTraceServer)(this.store, {
|
|
264
|
+
...this.config,
|
|
265
|
+
allowPortFallback: !this.portWasExplicit,
|
|
266
|
+
});
|
|
167
267
|
this.serverInfo = await this.server.start();
|
|
168
268
|
if (this.serverInfo && !this.uiWatcher) {
|
|
169
269
|
this.uiWatcher = await (0, ui_build_1.maybeStartUIWatcher)(() => {
|
|
170
270
|
this.server?.broadcast({
|
|
171
271
|
timestamp: new Date().toISOString(),
|
|
172
|
-
|
|
272
|
+
spanId: null,
|
|
173
273
|
type: 'ui:reload',
|
|
174
274
|
});
|
|
175
275
|
}, this.config.uiHotReload);
|
|
@@ -191,30 +291,273 @@ class LocalLLMTracerImpl {
|
|
|
191
291
|
})();
|
|
192
292
|
return this.serverStartPromise;
|
|
193
293
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
294
|
+
}
|
|
295
|
+
function normaliseRequest(request) {
|
|
296
|
+
return {
|
|
297
|
+
input: (0, utils_1.safeClone)(request?.input),
|
|
298
|
+
options: (0, utils_1.safeClone)(request?.options),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function bindMethod(target, value) {
|
|
302
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
303
|
+
}
|
|
304
|
+
function getConfiguredPort(configPort, envPort, currentExplicitPort) {
|
|
305
|
+
if (typeof configPort === 'number' && Number.isFinite(configPort)) {
|
|
306
|
+
return configPort;
|
|
197
307
|
}
|
|
198
|
-
|
|
199
|
-
|
|
308
|
+
if (envPort !== undefined) {
|
|
309
|
+
const parsed = Number(envPort);
|
|
310
|
+
if (Number.isFinite(parsed)) {
|
|
311
|
+
return parsed;
|
|
312
|
+
}
|
|
200
313
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
314
|
+
return typeof currentExplicitPort === 'number' && Number.isFinite(currentExplicitPort) ? currentExplicitPort : undefined;
|
|
315
|
+
}
|
|
316
|
+
function withOpenAITraceContext(context, params) {
|
|
317
|
+
return {
|
|
318
|
+
...(context || {}),
|
|
319
|
+
model: context?.model || (typeof params?.model === 'string' ? params.model : null),
|
|
320
|
+
provider: context?.provider || 'openai',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function wrapOpenAIChatCompletionsStream(stream, tracer, traceId) {
|
|
324
|
+
const state = {
|
|
325
|
+
content: '',
|
|
326
|
+
finished: false,
|
|
327
|
+
finishReasons: [],
|
|
328
|
+
began: false,
|
|
329
|
+
role: null,
|
|
330
|
+
toolCalls: new Map(),
|
|
331
|
+
usage: null,
|
|
332
|
+
};
|
|
333
|
+
const emitBegin = (role) => {
|
|
334
|
+
if (state.began) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
state.began = true;
|
|
338
|
+
const nextRole = role || state.role || 'assistant';
|
|
339
|
+
state.role = nextRole;
|
|
340
|
+
tracer.addSpanEvent(traceId, (0, utils_1.toSpanEventInputFromChunk)({ type: 'begin', role: nextRole }));
|
|
341
|
+
};
|
|
342
|
+
const emitFinish = () => {
|
|
343
|
+
if (state.finished) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
emitBegin(state.role);
|
|
347
|
+
state.finished = true;
|
|
348
|
+
tracer.endSpan(traceId, {
|
|
349
|
+
type: 'finish',
|
|
350
|
+
finish_reasons: state.finishReasons,
|
|
351
|
+
message: {
|
|
352
|
+
role: state.role || 'assistant',
|
|
353
|
+
content: state.content || null,
|
|
354
|
+
},
|
|
355
|
+
tool_calls: serializeOpenAIToolCalls(state.toolCalls),
|
|
356
|
+
usage: (0, utils_1.safeClone)(state.usage),
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
const processChunk = (chunk) => {
|
|
360
|
+
const raw = (0, utils_1.safeClone)(chunk);
|
|
361
|
+
const chunkToolCalls = [];
|
|
362
|
+
const finishReasons = new Set();
|
|
363
|
+
const contentParts = [];
|
|
364
|
+
const choices = Array.isArray(chunk?.choices) ? chunk.choices : [];
|
|
365
|
+
let sawEvent = false;
|
|
366
|
+
for (const choice of choices) {
|
|
367
|
+
const delta = choice?.delta || {};
|
|
368
|
+
if (typeof delta?.role === 'string' && delta.role) {
|
|
369
|
+
state.role = delta.role;
|
|
370
|
+
sawEvent = true;
|
|
371
|
+
}
|
|
372
|
+
for (const part of extractOpenAITextParts(delta?.content)) {
|
|
373
|
+
state.content = `${state.content}${part}`;
|
|
374
|
+
contentParts.push(part);
|
|
375
|
+
sawEvent = true;
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(delta?.tool_calls) && delta.tool_calls.length > 0) {
|
|
378
|
+
sawEvent = true;
|
|
379
|
+
for (const toolCall of delta.tool_calls) {
|
|
380
|
+
mergeOpenAIToolCallDelta(state.toolCalls, toolCall);
|
|
381
|
+
chunkToolCalls.push((0, utils_1.safeClone)(toolCall));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (typeof choice?.finish_reason === 'string' && choice.finish_reason) {
|
|
385
|
+
finishReasons.add(choice.finish_reason);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const usage = normalizeOpenAIUsage(chunk?.usage);
|
|
389
|
+
if (usage) {
|
|
390
|
+
state.usage = usage;
|
|
391
|
+
sawEvent = true;
|
|
392
|
+
}
|
|
393
|
+
for (const finishReason of finishReasons) {
|
|
394
|
+
state.finishReasons.push(finishReason);
|
|
395
|
+
}
|
|
396
|
+
if (sawEvent) {
|
|
397
|
+
emitBegin(state.role);
|
|
398
|
+
}
|
|
399
|
+
if (contentParts.length > 0) {
|
|
400
|
+
tracer.addSpanEvent(traceId, (0, utils_1.toSpanEventInputFromChunk)({
|
|
401
|
+
type: 'chunk',
|
|
402
|
+
content: contentParts.join(''),
|
|
403
|
+
finish_reasons: [...finishReasons],
|
|
404
|
+
raw,
|
|
405
|
+
tool_calls: chunkToolCalls,
|
|
406
|
+
usage,
|
|
407
|
+
}));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
tracer.addSpanEvent(traceId, (0, utils_1.toSpanEventInputFromChunk)({
|
|
411
|
+
type: 'event',
|
|
412
|
+
finish_reasons: [...finishReasons],
|
|
413
|
+
raw,
|
|
414
|
+
tool_calls: chunkToolCalls,
|
|
415
|
+
usage,
|
|
416
|
+
}));
|
|
417
|
+
};
|
|
418
|
+
const createWrappedIterator = (iterator) => ({
|
|
419
|
+
async next(...args) {
|
|
420
|
+
try {
|
|
421
|
+
const result = await iterator.next(...args);
|
|
422
|
+
if (result.done) {
|
|
423
|
+
emitFinish();
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
processChunk(result.value);
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
tracer.recordException(traceId, error);
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
async return(value) {
|
|
435
|
+
try {
|
|
436
|
+
const result = typeof iterator.return === 'function'
|
|
437
|
+
? await iterator.return(value)
|
|
438
|
+
: {
|
|
439
|
+
done: true,
|
|
440
|
+
value,
|
|
441
|
+
};
|
|
442
|
+
emitFinish();
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
tracer.recordException(traceId, error);
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
async throw(error) {
|
|
451
|
+
tracer.recordException(traceId, error);
|
|
452
|
+
if (typeof iterator.throw === 'function') {
|
|
453
|
+
return iterator.throw(error);
|
|
454
|
+
}
|
|
455
|
+
throw error;
|
|
456
|
+
},
|
|
457
|
+
[Symbol.asyncIterator]() {
|
|
458
|
+
return this;
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
return new Proxy(stream, {
|
|
462
|
+
get(target, prop, receiver) {
|
|
463
|
+
if (prop === Symbol.asyncIterator) {
|
|
464
|
+
return () => createWrappedIterator(target[Symbol.asyncIterator]());
|
|
465
|
+
}
|
|
466
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
function normalizeOpenAIChatCompletionResponse(response) {
|
|
471
|
+
const message = response?.choices?.[0]?.message;
|
|
472
|
+
return {
|
|
473
|
+
finish_reason: response?.choices?.[0]?.finish_reason || null,
|
|
474
|
+
id: response?.id || null,
|
|
475
|
+
message: normalizeOpenAIMessage(message),
|
|
476
|
+
model: response?.model || null,
|
|
477
|
+
object: response?.object || null,
|
|
478
|
+
raw: (0, utils_1.safeClone)(response),
|
|
479
|
+
tool_calls: Array.isArray(message?.tool_calls) ? (0, utils_1.safeClone)(message.tool_calls) : [],
|
|
480
|
+
usage: normalizeOpenAIUsage(response?.usage),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function normalizeOpenAIMessage(message) {
|
|
484
|
+
return {
|
|
485
|
+
...(0, utils_1.safeClone)(message),
|
|
486
|
+
content: normalizeOpenAIMessageContent(message),
|
|
487
|
+
role: typeof message?.role === 'string' ? message.role : 'assistant',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function normalizeOpenAIMessageContent(message) {
|
|
491
|
+
const content = extractOpenAITextParts(message?.content);
|
|
492
|
+
if (content.length > 0) {
|
|
493
|
+
return content.join('');
|
|
204
494
|
}
|
|
205
|
-
|
|
206
|
-
|
|
495
|
+
if (typeof message?.refusal === 'string' && message.refusal) {
|
|
496
|
+
return message.refusal;
|
|
207
497
|
}
|
|
208
|
-
|
|
209
|
-
|
|
498
|
+
return message?.content ?? null;
|
|
499
|
+
}
|
|
500
|
+
function extractOpenAITextParts(content) {
|
|
501
|
+
if (typeof content === 'string' && content) {
|
|
502
|
+
return [content];
|
|
503
|
+
}
|
|
504
|
+
if (!Array.isArray(content)) {
|
|
505
|
+
return [];
|
|
210
506
|
}
|
|
211
|
-
|
|
212
|
-
|
|
507
|
+
const parts = [];
|
|
508
|
+
for (const item of content) {
|
|
509
|
+
if (typeof item === 'string' && item) {
|
|
510
|
+
parts.push(item);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (typeof item?.text === 'string' && item.text) {
|
|
514
|
+
parts.push(item.text);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (typeof item?.content === 'string' && item.content) {
|
|
518
|
+
parts.push(item.content);
|
|
519
|
+
}
|
|
213
520
|
}
|
|
521
|
+
return parts;
|
|
214
522
|
}
|
|
215
|
-
function
|
|
523
|
+
function normalizeOpenAIUsage(usage) {
|
|
524
|
+
if (!usage || typeof usage !== 'object') {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
216
527
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
528
|
+
raw: (0, utils_1.safeClone)(usage),
|
|
529
|
+
tokens: {
|
|
530
|
+
completion: typeof usage?.completion_tokens === 'number' ? usage.completion_tokens : null,
|
|
531
|
+
prompt: typeof usage?.prompt_tokens === 'number' ? usage.prompt_tokens : null,
|
|
532
|
+
total: typeof usage?.total_tokens === 'number' ? usage.total_tokens : null,
|
|
533
|
+
},
|
|
219
534
|
};
|
|
220
535
|
}
|
|
536
|
+
function mergeOpenAIToolCallDelta(target, delta) {
|
|
537
|
+
const index = Number.isInteger(delta?.index) ? delta.index : target.size;
|
|
538
|
+
const current = (0, utils_1.safeClone)(target.get(index) || { function: { arguments: '' } });
|
|
539
|
+
if (delta?.id) {
|
|
540
|
+
current.id = delta.id;
|
|
541
|
+
}
|
|
542
|
+
if (delta?.type) {
|
|
543
|
+
current.type = delta.type;
|
|
544
|
+
}
|
|
545
|
+
if (delta?.function?.name) {
|
|
546
|
+
current.function = {
|
|
547
|
+
...(current.function || {}),
|
|
548
|
+
name: delta.function.name,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (delta?.function?.arguments) {
|
|
552
|
+
current.function = {
|
|
553
|
+
...(current.function || {}),
|
|
554
|
+
arguments: `${current.function?.arguments || ''}${delta.function.arguments}`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
target.set(index, current);
|
|
558
|
+
}
|
|
559
|
+
function serializeOpenAIToolCalls(toolCalls) {
|
|
560
|
+
return [...toolCalls.entries()]
|
|
561
|
+
.sort(([left], [right]) => left - right)
|
|
562
|
+
.map(([, value]) => (0, utils_1.safeClone)(value));
|
|
563
|
+
}
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -11,11 +11,12 @@ const node_url_1 = require("node:url");
|
|
|
11
11
|
const ui_1 = require("./ui");
|
|
12
12
|
function createTraceServer(store, options = {}) {
|
|
13
13
|
const host = options.host || '127.0.0.1';
|
|
14
|
-
const
|
|
14
|
+
const requestedPort = toPortNumber(options.port, 4319);
|
|
15
|
+
let activePort = requestedPort;
|
|
15
16
|
const clients = new Set();
|
|
16
17
|
const server = node_http_1.default.createServer(async (req, res) => {
|
|
17
18
|
try {
|
|
18
|
-
const url = new node_url_1.URL(req.url || '/', `http://${req.headers.host || `${host}:${
|
|
19
|
+
const url = new node_url_1.URL(req.url || '/', `http://${req.headers.host || `${host}:${activePort}`}`);
|
|
19
20
|
if (req.method === 'GET' && url.pathname === '/') {
|
|
20
21
|
sendHtml(res, (0, ui_1.renderAppHtml)());
|
|
21
22
|
return;
|
|
@@ -62,18 +63,13 @@ function createTraceServer(store, options = {}) {
|
|
|
62
63
|
});
|
|
63
64
|
return {
|
|
64
65
|
async start() {
|
|
65
|
-
await
|
|
66
|
-
|
|
67
|
-
server.listen(port, host, () => {
|
|
68
|
-
server.removeListener('error', reject);
|
|
69
|
-
resolve();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
66
|
+
await listenWithFallback(server, host, requestedPort, options.allowPortFallback);
|
|
67
|
+
activePort = getBoundPort(server, requestedPort);
|
|
72
68
|
server.unref();
|
|
73
69
|
return {
|
|
74
70
|
host,
|
|
75
|
-
port,
|
|
76
|
-
url: `http://${host}:${
|
|
71
|
+
port: activePort,
|
|
72
|
+
url: `http://${host}:${activePort}`,
|
|
77
73
|
};
|
|
78
74
|
},
|
|
79
75
|
broadcast,
|
|
@@ -92,6 +88,41 @@ function createTraceServer(store, options = {}) {
|
|
|
92
88
|
}
|
|
93
89
|
}
|
|
94
90
|
}
|
|
91
|
+
async function listenWithFallback(server, host, port, allowPortFallback = false) {
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
const tryListen = (nextPort, canFallback) => {
|
|
94
|
+
const onError = (error) => {
|
|
95
|
+
server.removeListener('listening', onListening);
|
|
96
|
+
if (canFallback && error?.code === 'EADDRINUSE') {
|
|
97
|
+
tryListen(0, false);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
reject(error);
|
|
101
|
+
};
|
|
102
|
+
const onListening = () => {
|
|
103
|
+
server.removeListener('error', onError);
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
server.once('error', onError);
|
|
107
|
+
server.once('listening', onListening);
|
|
108
|
+
server.listen(nextPort, host);
|
|
109
|
+
};
|
|
110
|
+
tryListen(port, allowPortFallback);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function getBoundPort(server, fallbackPort) {
|
|
114
|
+
const address = server.address();
|
|
115
|
+
if (address && typeof address === 'object' && typeof address.port === 'number') {
|
|
116
|
+
return address.port;
|
|
117
|
+
}
|
|
118
|
+
return fallbackPort;
|
|
119
|
+
}
|
|
120
|
+
function toPortNumber(value, fallback) {
|
|
121
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
return fallback;
|
|
125
|
+
}
|
|
95
126
|
function parseFilters(url) {
|
|
96
127
|
return {
|
|
97
128
|
search: url.searchParams.get('search') || undefined,
|
package/dist/session-nav.d.ts
CHANGED
|
@@ -31,4 +31,14 @@ export type SessionNavItem = {
|
|
|
31
31
|
shortSessionId: string;
|
|
32
32
|
status: "error" | "ok" | "pending";
|
|
33
33
|
};
|
|
34
|
+
export type SessionTreeSelection = {
|
|
35
|
+
selectedNodeId: string | null;
|
|
36
|
+
selectedTraceId: string | null;
|
|
37
|
+
};
|
|
34
38
|
export declare function deriveSessionNavItems(sessionNodes: SessionNavHierarchyNode[], traceById: Map<string, SessionNavTraceSummary>): SessionNavItem[];
|
|
39
|
+
export declare function sortSessionNodesForNav(sessionNodes: SessionNavHierarchyNode[], traceById: Map<string, SessionNavTraceSummary>): SessionNavHierarchyNode[];
|
|
40
|
+
export declare function findSessionNodePath(nodes: SessionNavHierarchyNode[], id: string, trail?: SessionNavHierarchyNode[]): SessionNavHierarchyNode[];
|
|
41
|
+
export declare function findSessionNodeById(nodes: SessionNavHierarchyNode[], id: string): SessionNavHierarchyNode | null;
|
|
42
|
+
export declare function getNewestTraceIdForNode(node: SessionNavHierarchyNode | null | undefined): string | null;
|
|
43
|
+
export declare function resolveSessionTreeSelection(sessionNodes: SessionNavHierarchyNode[], selectedNodeId: string | null, selectedTraceId: string | null): SessionTreeSelection;
|
|
44
|
+
export declare function getDefaultExpandedSessionTreeNodeIds(sessionNodes: SessionNavHierarchyNode[], activeSessionId: string | null, selectedNodeId: string | null): Set<string>;
|