@mtharrison/loupe 1.1.1 → 1.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 +103 -12
- package/assets/screenshot1.png +0 -0
- package/assets/screenshot2.png +0 -0
- package/dist/client/app.css +365 -263
- package/dist/client/app.js +803 -648
- package/dist/index.d.ts +3 -2
- package/dist/index.js +330 -2
- 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/types.d.ts +18 -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 TraceConfig, type TraceContext, type TraceRequest } from './types';
|
|
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 TraceConfig, type TraceContext, type TraceRequest } from './types';
|
|
2
|
+
export type { ChatModelLike, HierarchyNode, HierarchyResponse, LocalLLMTracer, NormalizedTraceContext, OpenAIChatCompletionCreateParamsLike, OpenAIChatCompletionStreamLike, OpenAIClientLike, 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<{
|
|
@@ -15,3 +15,4 @@ export declare function recordStreamFinish(traceId: string, chunk: unknown, conf
|
|
|
15
15
|
export declare function recordError(traceId: string, error: unknown, config?: TraceConfig): void;
|
|
16
16
|
export declare function __resetLocalLLMTracerForTests(): void;
|
|
17
17
|
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;
|
|
18
|
+
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
|
@@ -11,11 +11,13 @@ exports.recordStreamFinish = recordStreamFinish;
|
|
|
11
11
|
exports.recordError = recordError;
|
|
12
12
|
exports.__resetLocalLLMTracerForTests = __resetLocalLLMTracerForTests;
|
|
13
13
|
exports.wrapChatModel = wrapChatModel;
|
|
14
|
+
exports.wrapOpenAIClient = wrapOpenAIClient;
|
|
14
15
|
const server_1 = require("./server");
|
|
15
16
|
const store_1 = require("./store");
|
|
16
17
|
const ui_build_1 = require("./ui-build");
|
|
17
18
|
const utils_1 = require("./utils");
|
|
18
19
|
let singleton = null;
|
|
20
|
+
const DEFAULT_TRACE_PORT = 4319;
|
|
19
21
|
function isTraceEnabled() {
|
|
20
22
|
return (0, utils_1.envFlag)('LLM_TRACE_ENABLED');
|
|
21
23
|
}
|
|
@@ -105,9 +107,66 @@ function wrapChatModel(model, getContext, config) {
|
|
|
105
107
|
},
|
|
106
108
|
};
|
|
107
109
|
}
|
|
110
|
+
function wrapOpenAIClient(client, getContext, config) {
|
|
111
|
+
if (!client || typeof client.chat?.completions?.create !== 'function') {
|
|
112
|
+
throw new TypeError('wrapOpenAIClient expects an OpenAI client with chat.completions.create().');
|
|
113
|
+
}
|
|
114
|
+
const wrappedCompletions = new Proxy(client.chat.completions, {
|
|
115
|
+
get(target, prop, receiver) {
|
|
116
|
+
if (prop === 'create') {
|
|
117
|
+
return async (params, options) => {
|
|
118
|
+
const tracer = getLocalLLMTracer(config);
|
|
119
|
+
if (!tracer.isEnabled()) {
|
|
120
|
+
return target.create.call(target, params, options);
|
|
121
|
+
}
|
|
122
|
+
const context = withOpenAITraceContext(getContext ? getContext() : {}, params);
|
|
123
|
+
if (params?.stream) {
|
|
124
|
+
const traceId = tracer.recordStreamStart(context, { input: params, options: options });
|
|
125
|
+
try {
|
|
126
|
+
const stream = await target.create.call(target, params, options);
|
|
127
|
+
return wrapOpenAIChatCompletionsStream(stream, tracer, traceId);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
tracer.recordError(traceId, error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const traceId = tracer.recordInvokeStart(context, { input: params, options: options });
|
|
135
|
+
try {
|
|
136
|
+
const response = await target.create.call(target, params, options);
|
|
137
|
+
tracer.recordInvokeFinish(traceId, normalizeOpenAIChatCompletionResponse(response));
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
tracer.recordError(traceId, error);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const wrappedChat = new Proxy(client.chat, {
|
|
150
|
+
get(target, prop, receiver) {
|
|
151
|
+
if (prop === 'completions') {
|
|
152
|
+
return wrappedCompletions;
|
|
153
|
+
}
|
|
154
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return new Proxy(client, {
|
|
158
|
+
get(target, prop, receiver) {
|
|
159
|
+
if (prop === 'chat') {
|
|
160
|
+
return wrappedChat;
|
|
161
|
+
}
|
|
162
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
108
166
|
class LocalLLMTracerImpl {
|
|
109
167
|
config;
|
|
110
168
|
loggedUrl;
|
|
169
|
+
portWasExplicit;
|
|
111
170
|
server;
|
|
112
171
|
serverFailed;
|
|
113
172
|
serverInfo;
|
|
@@ -121,6 +180,7 @@ class LocalLLMTracerImpl {
|
|
|
121
180
|
port: 4319,
|
|
122
181
|
uiHotReload: false,
|
|
123
182
|
};
|
|
183
|
+
this.portWasExplicit = false;
|
|
124
184
|
this.configure(config);
|
|
125
185
|
this.store = new store_1.TraceStore({ maxTraces: this.config.maxTraces });
|
|
126
186
|
this.server = null;
|
|
@@ -134,9 +194,11 @@ class LocalLLMTracerImpl {
|
|
|
134
194
|
if (this.serverInfo && (config.host || config.port)) {
|
|
135
195
|
return;
|
|
136
196
|
}
|
|
197
|
+
const explicitPort = getConfiguredPort(config.port, process.env.LLM_TRACE_PORT, this.portWasExplicit ? this.config.port : undefined);
|
|
198
|
+
this.portWasExplicit = explicitPort !== undefined;
|
|
137
199
|
this.config = {
|
|
138
200
|
host: config.host || this.config.host || process.env.LLM_TRACE_HOST || '127.0.0.1',
|
|
139
|
-
port:
|
|
201
|
+
port: explicitPort ?? DEFAULT_TRACE_PORT,
|
|
140
202
|
maxTraces: Number(config.maxTraces || this.config.maxTraces || process.env.LLM_TRACE_MAX_TRACES) || 1000,
|
|
141
203
|
uiHotReload: typeof config.uiHotReload === 'boolean'
|
|
142
204
|
? config.uiHotReload
|
|
@@ -163,7 +225,10 @@ class LocalLLMTracerImpl {
|
|
|
163
225
|
}
|
|
164
226
|
this.serverStartPromise = (async () => {
|
|
165
227
|
try {
|
|
166
|
-
this.server = (0, server_1.createTraceServer)(this.store,
|
|
228
|
+
this.server = (0, server_1.createTraceServer)(this.store, {
|
|
229
|
+
...this.config,
|
|
230
|
+
allowPortFallback: !this.portWasExplicit,
|
|
231
|
+
});
|
|
167
232
|
this.serverInfo = await this.server.start();
|
|
168
233
|
if (this.serverInfo && !this.uiWatcher) {
|
|
169
234
|
this.uiWatcher = await (0, ui_build_1.maybeStartUIWatcher)(() => {
|
|
@@ -218,3 +283,266 @@ function normaliseRequest(request) {
|
|
|
218
283
|
options: (0, utils_1.safeClone)(request?.options),
|
|
219
284
|
};
|
|
220
285
|
}
|
|
286
|
+
function bindMethod(target, value) {
|
|
287
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
288
|
+
}
|
|
289
|
+
function getConfiguredPort(configPort, envPort, currentExplicitPort) {
|
|
290
|
+
if (typeof configPort === 'number' && Number.isFinite(configPort)) {
|
|
291
|
+
return configPort;
|
|
292
|
+
}
|
|
293
|
+
if (envPort !== undefined) {
|
|
294
|
+
const parsed = Number(envPort);
|
|
295
|
+
if (Number.isFinite(parsed)) {
|
|
296
|
+
return parsed;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return typeof currentExplicitPort === 'number' && Number.isFinite(currentExplicitPort) ? currentExplicitPort : undefined;
|
|
300
|
+
}
|
|
301
|
+
function withOpenAITraceContext(context, params) {
|
|
302
|
+
return {
|
|
303
|
+
...(context || {}),
|
|
304
|
+
model: context?.model || (typeof params?.model === 'string' ? params.model : null),
|
|
305
|
+
provider: context?.provider || 'openai',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function wrapOpenAIChatCompletionsStream(stream, tracer, traceId) {
|
|
309
|
+
const state = {
|
|
310
|
+
content: '',
|
|
311
|
+
finished: false,
|
|
312
|
+
finishReasons: [],
|
|
313
|
+
began: false,
|
|
314
|
+
role: null,
|
|
315
|
+
toolCalls: new Map(),
|
|
316
|
+
usage: null,
|
|
317
|
+
};
|
|
318
|
+
const emitBegin = (role) => {
|
|
319
|
+
if (state.began) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
state.began = true;
|
|
323
|
+
const nextRole = role || state.role || 'assistant';
|
|
324
|
+
state.role = nextRole;
|
|
325
|
+
tracer.recordStreamChunk(traceId, { type: 'begin', role: nextRole });
|
|
326
|
+
};
|
|
327
|
+
const emitFinish = () => {
|
|
328
|
+
if (state.finished) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
emitBegin(state.role);
|
|
332
|
+
state.finished = true;
|
|
333
|
+
tracer.recordStreamFinish(traceId, {
|
|
334
|
+
type: 'finish',
|
|
335
|
+
finish_reasons: state.finishReasons,
|
|
336
|
+
message: {
|
|
337
|
+
role: state.role || 'assistant',
|
|
338
|
+
content: state.content || null,
|
|
339
|
+
},
|
|
340
|
+
tool_calls: serializeOpenAIToolCalls(state.toolCalls),
|
|
341
|
+
usage: (0, utils_1.safeClone)(state.usage),
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
const processChunk = (chunk) => {
|
|
345
|
+
const raw = (0, utils_1.safeClone)(chunk);
|
|
346
|
+
const chunkToolCalls = [];
|
|
347
|
+
const finishReasons = new Set();
|
|
348
|
+
const contentParts = [];
|
|
349
|
+
const choices = Array.isArray(chunk?.choices) ? chunk.choices : [];
|
|
350
|
+
let sawEvent = false;
|
|
351
|
+
for (const choice of choices) {
|
|
352
|
+
const delta = choice?.delta || {};
|
|
353
|
+
if (typeof delta?.role === 'string' && delta.role) {
|
|
354
|
+
state.role = delta.role;
|
|
355
|
+
sawEvent = true;
|
|
356
|
+
}
|
|
357
|
+
for (const part of extractOpenAITextParts(delta?.content)) {
|
|
358
|
+
state.content = `${state.content}${part}`;
|
|
359
|
+
contentParts.push(part);
|
|
360
|
+
sawEvent = true;
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(delta?.tool_calls) && delta.tool_calls.length > 0) {
|
|
363
|
+
sawEvent = true;
|
|
364
|
+
for (const toolCall of delta.tool_calls) {
|
|
365
|
+
mergeOpenAIToolCallDelta(state.toolCalls, toolCall);
|
|
366
|
+
chunkToolCalls.push((0, utils_1.safeClone)(toolCall));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (typeof choice?.finish_reason === 'string' && choice.finish_reason) {
|
|
370
|
+
finishReasons.add(choice.finish_reason);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const usage = normalizeOpenAIUsage(chunk?.usage);
|
|
374
|
+
if (usage) {
|
|
375
|
+
state.usage = usage;
|
|
376
|
+
sawEvent = true;
|
|
377
|
+
}
|
|
378
|
+
for (const finishReason of finishReasons) {
|
|
379
|
+
state.finishReasons.push(finishReason);
|
|
380
|
+
}
|
|
381
|
+
if (sawEvent) {
|
|
382
|
+
emitBegin(state.role);
|
|
383
|
+
}
|
|
384
|
+
if (contentParts.length > 0) {
|
|
385
|
+
tracer.recordStreamChunk(traceId, {
|
|
386
|
+
type: 'chunk',
|
|
387
|
+
content: contentParts.join(''),
|
|
388
|
+
finish_reasons: [...finishReasons],
|
|
389
|
+
raw,
|
|
390
|
+
tool_calls: chunkToolCalls,
|
|
391
|
+
usage,
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
tracer.recordStreamChunk(traceId, {
|
|
396
|
+
type: 'event',
|
|
397
|
+
finish_reasons: [...finishReasons],
|
|
398
|
+
raw,
|
|
399
|
+
tool_calls: chunkToolCalls,
|
|
400
|
+
usage,
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
const createWrappedIterator = (iterator) => ({
|
|
404
|
+
async next(...args) {
|
|
405
|
+
try {
|
|
406
|
+
const result = await iterator.next(...args);
|
|
407
|
+
if (result.done) {
|
|
408
|
+
emitFinish();
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
processChunk(result.value);
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
tracer.recordError(traceId, error);
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
async return(value) {
|
|
420
|
+
try {
|
|
421
|
+
const result = typeof iterator.return === 'function'
|
|
422
|
+
? await iterator.return(value)
|
|
423
|
+
: {
|
|
424
|
+
done: true,
|
|
425
|
+
value,
|
|
426
|
+
};
|
|
427
|
+
emitFinish();
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
tracer.recordError(traceId, error);
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
async throw(error) {
|
|
436
|
+
tracer.recordError(traceId, error);
|
|
437
|
+
if (typeof iterator.throw === 'function') {
|
|
438
|
+
return iterator.throw(error);
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
},
|
|
442
|
+
[Symbol.asyncIterator]() {
|
|
443
|
+
return this;
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
return new Proxy(stream, {
|
|
447
|
+
get(target, prop, receiver) {
|
|
448
|
+
if (prop === Symbol.asyncIterator) {
|
|
449
|
+
return () => createWrappedIterator(target[Symbol.asyncIterator]());
|
|
450
|
+
}
|
|
451
|
+
return bindMethod(target, Reflect.get(target, prop, receiver));
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function normalizeOpenAIChatCompletionResponse(response) {
|
|
456
|
+
const message = response?.choices?.[0]?.message;
|
|
457
|
+
return {
|
|
458
|
+
finish_reason: response?.choices?.[0]?.finish_reason || null,
|
|
459
|
+
id: response?.id || null,
|
|
460
|
+
message: normalizeOpenAIMessage(message),
|
|
461
|
+
model: response?.model || null,
|
|
462
|
+
object: response?.object || null,
|
|
463
|
+
raw: (0, utils_1.safeClone)(response),
|
|
464
|
+
tool_calls: Array.isArray(message?.tool_calls) ? (0, utils_1.safeClone)(message.tool_calls) : [],
|
|
465
|
+
usage: normalizeOpenAIUsage(response?.usage),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function normalizeOpenAIMessage(message) {
|
|
469
|
+
return {
|
|
470
|
+
...(0, utils_1.safeClone)(message),
|
|
471
|
+
content: normalizeOpenAIMessageContent(message),
|
|
472
|
+
role: typeof message?.role === 'string' ? message.role : 'assistant',
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function normalizeOpenAIMessageContent(message) {
|
|
476
|
+
const content = extractOpenAITextParts(message?.content);
|
|
477
|
+
if (content.length > 0) {
|
|
478
|
+
return content.join('');
|
|
479
|
+
}
|
|
480
|
+
if (typeof message?.refusal === 'string' && message.refusal) {
|
|
481
|
+
return message.refusal;
|
|
482
|
+
}
|
|
483
|
+
return message?.content ?? null;
|
|
484
|
+
}
|
|
485
|
+
function extractOpenAITextParts(content) {
|
|
486
|
+
if (typeof content === 'string' && content) {
|
|
487
|
+
return [content];
|
|
488
|
+
}
|
|
489
|
+
if (!Array.isArray(content)) {
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
const parts = [];
|
|
493
|
+
for (const item of content) {
|
|
494
|
+
if (typeof item === 'string' && item) {
|
|
495
|
+
parts.push(item);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (typeof item?.text === 'string' && item.text) {
|
|
499
|
+
parts.push(item.text);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (typeof item?.content === 'string' && item.content) {
|
|
503
|
+
parts.push(item.content);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return parts;
|
|
507
|
+
}
|
|
508
|
+
function normalizeOpenAIUsage(usage) {
|
|
509
|
+
if (!usage || typeof usage !== 'object') {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
raw: (0, utils_1.safeClone)(usage),
|
|
514
|
+
tokens: {
|
|
515
|
+
completion: typeof usage?.completion_tokens === 'number' ? usage.completion_tokens : null,
|
|
516
|
+
prompt: typeof usage?.prompt_tokens === 'number' ? usage.prompt_tokens : null,
|
|
517
|
+
total: typeof usage?.total_tokens === 'number' ? usage.total_tokens : null,
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function mergeOpenAIToolCallDelta(target, delta) {
|
|
522
|
+
const index = Number.isInteger(delta?.index) ? delta.index : target.size;
|
|
523
|
+
const current = (0, utils_1.safeClone)(target.get(index) || { function: { arguments: '' } });
|
|
524
|
+
if (delta?.id) {
|
|
525
|
+
current.id = delta.id;
|
|
526
|
+
}
|
|
527
|
+
if (delta?.type) {
|
|
528
|
+
current.type = delta.type;
|
|
529
|
+
}
|
|
530
|
+
if (delta?.function?.name) {
|
|
531
|
+
current.function = {
|
|
532
|
+
...(current.function || {}),
|
|
533
|
+
name: delta.function.name,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (delta?.function?.arguments) {
|
|
537
|
+
current.function = {
|
|
538
|
+
...(current.function || {}),
|
|
539
|
+
arguments: `${current.function?.arguments || ''}${delta.function.arguments}`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
target.set(index, current);
|
|
543
|
+
}
|
|
544
|
+
function serializeOpenAIToolCalls(toolCalls) {
|
|
545
|
+
return [...toolCalls.entries()]
|
|
546
|
+
.sort(([left], [right]) => left - right)
|
|
547
|
+
.map(([, value]) => (0, utils_1.safeClone)(value));
|
|
548
|
+
}
|
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>;
|
package/dist/session-nav.js
CHANGED
|
@@ -1,11 +1,96 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.deriveSessionNavItems = deriveSessionNavItems;
|
|
4
|
+
exports.sortSessionNodesForNav = sortSessionNodesForNav;
|
|
5
|
+
exports.findSessionNodePath = findSessionNodePath;
|
|
6
|
+
exports.findSessionNodeById = findSessionNodeById;
|
|
7
|
+
exports.getNewestTraceIdForNode = getNewestTraceIdForNode;
|
|
8
|
+
exports.resolveSessionTreeSelection = resolveSessionTreeSelection;
|
|
9
|
+
exports.getDefaultExpandedSessionTreeNodeIds = getDefaultExpandedSessionTreeNodeIds;
|
|
4
10
|
function deriveSessionNavItems(sessionNodes, traceById) {
|
|
5
11
|
return sessionNodes
|
|
6
12
|
.map((node) => deriveSessionNavItem(node, traceById))
|
|
7
13
|
.sort(compareSessionNavItems);
|
|
8
14
|
}
|
|
15
|
+
function sortSessionNodesForNav(sessionNodes, traceById) {
|
|
16
|
+
const itemById = new Map(sessionNodes.map((node) => [node.id, deriveSessionNavItem(node, traceById)]));
|
|
17
|
+
return sessionNodes
|
|
18
|
+
.slice()
|
|
19
|
+
.sort((left, right) => compareSessionNavItems(itemById.get(left.id), itemById.get(right.id)));
|
|
20
|
+
}
|
|
21
|
+
function findSessionNodePath(nodes, id, trail = []) {
|
|
22
|
+
for (const node of nodes) {
|
|
23
|
+
const nextTrail = [...trail, node];
|
|
24
|
+
if (node.id === id) {
|
|
25
|
+
return nextTrail;
|
|
26
|
+
}
|
|
27
|
+
const childTrail = findSessionNodePath(node.children, id, nextTrail);
|
|
28
|
+
if (childTrail.length) {
|
|
29
|
+
return childTrail;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
function findSessionNodeById(nodes, id) {
|
|
35
|
+
return findSessionNodePath(nodes, id).at(-1) ?? null;
|
|
36
|
+
}
|
|
37
|
+
function getNewestTraceIdForNode(node) {
|
|
38
|
+
if (!node?.traceIds.length) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (typeof node.meta?.traceId === "string" && node.meta.traceId) {
|
|
42
|
+
return node.meta.traceId;
|
|
43
|
+
}
|
|
44
|
+
return node.traceIds[0] || null;
|
|
45
|
+
}
|
|
46
|
+
function resolveSessionTreeSelection(sessionNodes, selectedNodeId, selectedTraceId) {
|
|
47
|
+
const selectedNode = selectedNodeId
|
|
48
|
+
? findSessionNodeById(sessionNodes, selectedNodeId)
|
|
49
|
+
: null;
|
|
50
|
+
const selectedTraceNode = selectedTraceId
|
|
51
|
+
? findSessionNodeById(sessionNodes, `trace:${selectedTraceId}`)
|
|
52
|
+
: null;
|
|
53
|
+
const fallbackNode = selectedNode ?? selectedTraceNode ?? sessionNodes[0] ?? null;
|
|
54
|
+
if (!fallbackNode) {
|
|
55
|
+
return {
|
|
56
|
+
selectedNodeId: null,
|
|
57
|
+
selectedTraceId: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const nextSelectedNodeId = selectedNode?.id ?? fallbackNode.id;
|
|
61
|
+
const nextSelectedTraceId = selectedTraceId && fallbackNode.traceIds.includes(selectedTraceId)
|
|
62
|
+
? selectedTraceId
|
|
63
|
+
: getNewestTraceIdForNode(fallbackNode);
|
|
64
|
+
return {
|
|
65
|
+
selectedNodeId: nextSelectedNodeId,
|
|
66
|
+
selectedTraceId: nextSelectedTraceId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId) {
|
|
70
|
+
const expanded = new Set();
|
|
71
|
+
const activeSession = (activeSessionId
|
|
72
|
+
? sessionNodes.find((node) => node.id === activeSessionId) ?? null
|
|
73
|
+
: null) ?? sessionNodes[0] ?? null;
|
|
74
|
+
if (!activeSession) {
|
|
75
|
+
return expanded;
|
|
76
|
+
}
|
|
77
|
+
if (activeSession.children.length) {
|
|
78
|
+
expanded.add(activeSession.id);
|
|
79
|
+
}
|
|
80
|
+
visitSessionTree(activeSession.children, (node) => {
|
|
81
|
+
if (node.children.length && node.type === "actor") {
|
|
82
|
+
expanded.add(node.id);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (selectedNodeId) {
|
|
86
|
+
for (const node of findSessionNodePath([activeSession], selectedNodeId)) {
|
|
87
|
+
if (node.children.length) {
|
|
88
|
+
expanded.add(node.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return expanded;
|
|
93
|
+
}
|
|
9
94
|
function deriveSessionNavItem(node, traceById) {
|
|
10
95
|
const traces = node.traceIds
|
|
11
96
|
.map((traceId) => traceById.get(traceId))
|
|
@@ -130,3 +215,9 @@ function formatCompactTimestamp(value) {
|
|
|
130
215
|
minute: "2-digit",
|
|
131
216
|
});
|
|
132
217
|
}
|
|
218
|
+
function visitSessionTree(nodes, visitor) {
|
|
219
|
+
for (const node of nodes) {
|
|
220
|
+
visitor(node);
|
|
221
|
+
visitSessionTree(node.children, visitor);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -202,6 +202,24 @@ export interface ChatModelLike<TInput = any, TOptions = any, TValue = any, TChun
|
|
|
202
202
|
invoke(input: TInput, options?: TOptions): Promise<TValue>;
|
|
203
203
|
stream(input: TInput, options?: TOptions): AsyncGenerator<TChunk>;
|
|
204
204
|
}
|
|
205
|
+
export type OpenAIChatCompletionCreateParamsLike = Record<string, any> & {
|
|
206
|
+
messages?: Record<string, any>[];
|
|
207
|
+
model?: string | null;
|
|
208
|
+
stream?: boolean | null;
|
|
209
|
+
};
|
|
210
|
+
export interface OpenAIChatCompletionStreamLike<TChunk = any> extends AsyncIterable<TChunk> {
|
|
211
|
+
[Symbol.asyncIterator](): AsyncIterator<TChunk>;
|
|
212
|
+
}
|
|
213
|
+
export interface OpenAIChatCompletionsLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
|
|
214
|
+
create(params: TParams, options?: TOptions): Promise<TResponse> | Promise<OpenAIChatCompletionStreamLike<TChunk>> | OpenAIChatCompletionStreamLike<TChunk>;
|
|
215
|
+
}
|
|
216
|
+
export interface OpenAIClientLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
|
|
217
|
+
chat: {
|
|
218
|
+
completions: OpenAIChatCompletionsLike<TParams, TOptions, TResponse, TChunk>;
|
|
219
|
+
[key: string]: any;
|
|
220
|
+
};
|
|
221
|
+
[key: string]: any;
|
|
222
|
+
}
|
|
205
223
|
export type TraceServer = {
|
|
206
224
|
broadcast(event: TraceEvent | UIReloadEvent): void;
|
|
207
225
|
close(): void;
|