lightrace 0.1.1 → 0.1.3
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/dist/client.d.ts +27 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +100 -34
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +35 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +67 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/langchain.d.ts +15 -8
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +413 -209
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/observation.d.ts +6 -4
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +27 -36
- package/dist/observation.js.map +1 -1
- package/dist/otel-exporter.d.ts +39 -0
- package/dist/otel-exporter.d.ts.map +1 -0
- package/dist/otel-exporter.js +70 -0
- package/dist/otel-exporter.js.map +1 -0
- package/dist/tool-client.d.ts +14 -0
- package/dist/tool-client.d.ts.map +1 -1
- package/dist/tool-client.js +76 -25
- package/dist/tool-client.js.map +1 -1
- package/dist/trace.d.ts +10 -12
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js +135 -103
- package/dist/trace.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extends `BaseCallbackHandler` from `@langchain/core` to automatically
|
|
5
5
|
* capture chains, LLM calls, tool invocations, and retriever operations
|
|
6
|
-
* as Lightrace trace observations.
|
|
6
|
+
* as Lightrace trace observations via OpenTelemetry spans.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```ts
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
|
|
18
18
|
import { generateId, jsonSerializable } from "../utils.js";
|
|
19
|
+
import * as attrs from "../otel-exporter.js";
|
|
19
20
|
import { Lightrace } from "../client.js";
|
|
20
21
|
export class LightraceCallbackHandler extends BaseCallbackHandler {
|
|
21
22
|
name = "lightrace";
|
|
@@ -30,8 +31,10 @@ export class LightraceCallbackHandler extends BaseCallbackHandler {
|
|
|
30
31
|
sessionId;
|
|
31
32
|
traceName;
|
|
32
33
|
rootMetadata;
|
|
33
|
-
//
|
|
34
|
-
|
|
34
|
+
// OTel exporter
|
|
35
|
+
otelExporter = null;
|
|
36
|
+
/** Root span for the current trace. */
|
|
37
|
+
rootSpan = null;
|
|
35
38
|
/** The trace ID from the most recently completed root run. */
|
|
36
39
|
lastTraceId = null;
|
|
37
40
|
constructor(opts) {
|
|
@@ -41,23 +44,43 @@ export class LightraceCallbackHandler extends BaseCallbackHandler {
|
|
|
41
44
|
this.traceName = opts?.traceName;
|
|
42
45
|
this.rootMetadata = opts?.metadata;
|
|
43
46
|
const client = opts?.client ?? Lightrace.getInstance();
|
|
44
|
-
this.
|
|
47
|
+
this.otelExporter = client?.getOtelExporter() ?? null;
|
|
45
48
|
}
|
|
46
|
-
//
|
|
49
|
+
// -- helpers ----------------------------------------------------------------
|
|
47
50
|
getParentObservationId(observationId) {
|
|
48
51
|
return this.runParents.get(observationId) ?? null;
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Normalize IO data by converting BaseMessage-like objects to plain
|
|
55
|
+
* {role, content, tool_calls?} objects. Recurses into arrays and plain objects.
|
|
56
|
+
*/
|
|
57
|
+
normalizeIO(data) {
|
|
58
|
+
if (data === null || data === undefined)
|
|
59
|
+
return data;
|
|
60
|
+
if (Array.isArray(data))
|
|
61
|
+
return data.map((d) => this.normalizeIO(d));
|
|
62
|
+
if (typeof data === "object") {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const obj = data;
|
|
65
|
+
// Detect BaseMessage-like objects
|
|
66
|
+
if ("content" in obj && ("type" in obj || "_getType" in obj)) {
|
|
67
|
+
const role = typeof obj._getType === "function" ? obj._getType() : (obj.type ?? "unknown");
|
|
68
|
+
return {
|
|
69
|
+
role,
|
|
70
|
+
content: obj.content,
|
|
71
|
+
...(obj.tool_calls?.length ? { tool_calls: obj.tool_calls } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Recurse into plain objects
|
|
75
|
+
const result = {};
|
|
76
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
77
|
+
result[k] = this.normalizeIO(v);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
return data;
|
|
52
82
|
}
|
|
53
|
-
|
|
54
|
-
const eventTypeMap = {
|
|
55
|
-
span: "span-create",
|
|
56
|
-
generation: "generation-create",
|
|
57
|
-
event: "event-create",
|
|
58
|
-
tool: "tool-create",
|
|
59
|
-
chain: "chain-create",
|
|
60
|
-
};
|
|
83
|
+
endObservationSpan(run, output, level, statusMessage, extra) {
|
|
61
84
|
const obsTypeMap = {
|
|
62
85
|
span: "SPAN",
|
|
63
86
|
generation: "GENERATION",
|
|
@@ -65,62 +88,116 @@ export class LightraceCallbackHandler extends BaseCallbackHandler {
|
|
|
65
88
|
tool: "TOOL",
|
|
66
89
|
chain: "CHAIN",
|
|
67
90
|
};
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
statusMessage
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const span = run.span;
|
|
92
|
+
span.setAttribute(attrs.OBSERVATION_TYPE, obsTypeMap[run.type] ?? run.type);
|
|
93
|
+
span.setAttribute(attrs.OBSERVATION_INPUT, attrs.safeJson(jsonSerializable(run.input)));
|
|
94
|
+
span.setAttribute(attrs.OBSERVATION_OUTPUT, attrs.safeJson(jsonSerializable(output)));
|
|
95
|
+
span.setAttribute(attrs.OBSERVATION_LEVEL, level);
|
|
96
|
+
if (run.metadata) {
|
|
97
|
+
span.setAttribute(attrs.OBSERVATION_METADATA, attrs.safeJson(run.metadata));
|
|
98
|
+
}
|
|
99
|
+
if (run.model) {
|
|
100
|
+
span.setAttribute(attrs.OBSERVATION_MODEL, run.model);
|
|
101
|
+
}
|
|
102
|
+
if (statusMessage) {
|
|
103
|
+
span.setAttribute(attrs.OBSERVATION_STATUS_MESSAGE, statusMessage);
|
|
104
|
+
}
|
|
105
|
+
if (run.modelParameters && Object.keys(run.modelParameters).length > 0) {
|
|
106
|
+
span.setAttribute(attrs.OBSERVATION_MODEL_PARAMETERS, attrs.safeJson(run.modelParameters));
|
|
107
|
+
}
|
|
108
|
+
if (extra) {
|
|
109
|
+
// Usage details from extra (promptTokens, completionTokens, totalTokens)
|
|
110
|
+
const usageDetails = {};
|
|
111
|
+
if (extra.promptTokens !== undefined)
|
|
112
|
+
usageDetails.promptTokens = extra.promptTokens;
|
|
113
|
+
if (extra.completionTokens !== undefined)
|
|
114
|
+
usageDetails.completionTokens = extra.completionTokens;
|
|
115
|
+
if (extra.totalTokens !== undefined)
|
|
116
|
+
usageDetails.totalTokens = extra.totalTokens;
|
|
117
|
+
if (Object.keys(usageDetails).length > 0) {
|
|
118
|
+
span.setAttribute(attrs.OBSERVATION_USAGE_DETAILS, attrs.safeJson(usageDetails));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
span.end();
|
|
90
122
|
}
|
|
91
123
|
extractModelName(serialized) {
|
|
124
|
+
if (!serialized)
|
|
125
|
+
return undefined;
|
|
92
126
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
127
|
const s = serialized;
|
|
94
128
|
return (s?.kwargs?.model_name ?? s?.kwargs?.model ?? s?.kwargs?.modelName ?? s?.name ?? undefined);
|
|
95
129
|
}
|
|
130
|
+
extractModelParameters(extraParams) {
|
|
131
|
+
const invocationParams = extraParams?.["invocation_params"];
|
|
132
|
+
const modelParameters = {};
|
|
133
|
+
for (const key of [
|
|
134
|
+
"temperature",
|
|
135
|
+
"max_tokens",
|
|
136
|
+
"top_p",
|
|
137
|
+
"frequency_penalty",
|
|
138
|
+
"presence_penalty",
|
|
139
|
+
]) {
|
|
140
|
+
const val = invocationParams?.[key];
|
|
141
|
+
if (val !== undefined && val !== null)
|
|
142
|
+
modelParameters[key] = val;
|
|
143
|
+
}
|
|
144
|
+
return modelParameters;
|
|
145
|
+
}
|
|
96
146
|
isAgent(serialized) {
|
|
147
|
+
if (!serialized)
|
|
148
|
+
return false;
|
|
97
149
|
const name = (serialized.id?.join("/") ?? "").toLowerCase();
|
|
98
150
|
const sName = (serialized.name ?? "").toLowerCase();
|
|
99
151
|
return name.includes("agent") || sName.includes("agent");
|
|
100
152
|
}
|
|
101
153
|
registerRun(runId, parentRunId, info) {
|
|
154
|
+
const tracer = this.otelExporter?.tracer;
|
|
102
155
|
const isRoot = !this.rootRunId;
|
|
103
156
|
if (isRoot) {
|
|
104
157
|
this.rootRunId = runId;
|
|
105
158
|
this._traceId = generateId();
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
// Create root span representing the trace
|
|
160
|
+
if (tracer) {
|
|
161
|
+
this.rootSpan = tracer.startSpan(this.traceName ?? info.name, {
|
|
162
|
+
startTime: info.startTime,
|
|
163
|
+
});
|
|
164
|
+
this.rootSpan.setAttribute(attrs.AS_ROOT, "true");
|
|
165
|
+
this.rootSpan.setAttribute(attrs.TRACE_NAME, this.traceName ?? info.name);
|
|
166
|
+
this.rootSpan.setAttribute(attrs.TRACE_INPUT, attrs.safeJson(jsonSerializable(info.input)));
|
|
167
|
+
if (this.userId)
|
|
168
|
+
this.rootSpan.setAttribute(attrs.TRACE_USER_ID, this.userId);
|
|
169
|
+
if (this.sessionId)
|
|
170
|
+
this.rootSpan.setAttribute(attrs.TRACE_SESSION_ID, this.sessionId);
|
|
171
|
+
if (this.rootMetadata) {
|
|
172
|
+
this.rootSpan.setAttribute(attrs.TRACE_METADATA, attrs.safeJson(this.rootMetadata));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
121
175
|
}
|
|
122
176
|
const observationId = generateId();
|
|
123
|
-
|
|
177
|
+
// Create an OTel span for this observation
|
|
178
|
+
let span;
|
|
179
|
+
if (tracer) {
|
|
180
|
+
span = tracer.startSpan(info.name, { startTime: info.startTime });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Dummy span if no tracer -- will be a no-op
|
|
184
|
+
span = {
|
|
185
|
+
setAttribute: () => span,
|
|
186
|
+
setAttributes: () => span,
|
|
187
|
+
addEvent: () => span,
|
|
188
|
+
setStatus: () => span,
|
|
189
|
+
end: () => { },
|
|
190
|
+
isRecording: () => false,
|
|
191
|
+
recordException: () => { },
|
|
192
|
+
spanContext: () => ({
|
|
193
|
+
traceId: "",
|
|
194
|
+
spanId: "",
|
|
195
|
+
traceFlags: 0,
|
|
196
|
+
}),
|
|
197
|
+
updateName: () => span,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const runInfo = { ...info, observationId, parentRunId, span };
|
|
124
201
|
this.runs.set(runId, runInfo);
|
|
125
202
|
this.runParents.set(observationId, parentRunId ? this.runs.get(parentRunId)?.observationId : undefined);
|
|
126
203
|
return runInfo;
|
|
@@ -129,25 +206,14 @@ export class LightraceCallbackHandler extends BaseCallbackHandler {
|
|
|
129
206
|
const run = this.runs.get(runId);
|
|
130
207
|
if (!run)
|
|
131
208
|
return;
|
|
132
|
-
|
|
133
|
-
this
|
|
134
|
-
// If this is the root run completing, update the trace and reset
|
|
209
|
+
this.endObservationSpan(run, output, level, statusMessage, extra);
|
|
210
|
+
// If this is the root run completing, end the root span and reset
|
|
135
211
|
if (runId === this.rootRunId) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
body: {
|
|
142
|
-
id: this._traceId,
|
|
143
|
-
name: this.traceName ?? run.name,
|
|
144
|
-
timestamp: run.startTime.toISOString(),
|
|
145
|
-
output: jsonSerializable(output),
|
|
146
|
-
userId: this.userId,
|
|
147
|
-
sessionId: this.sessionId,
|
|
148
|
-
metadata: this.rootMetadata ?? null,
|
|
149
|
-
},
|
|
150
|
-
});
|
|
212
|
+
if (this.rootSpan) {
|
|
213
|
+
this.rootSpan.setAttribute(attrs.TRACE_OUTPUT, attrs.safeJson(jsonSerializable(output)));
|
|
214
|
+
this.rootSpan.end();
|
|
215
|
+
this.rootSpan = null;
|
|
216
|
+
}
|
|
151
217
|
this.lastTraceId = this._traceId;
|
|
152
218
|
this.rootRunId = null;
|
|
153
219
|
this._traceId = null;
|
|
@@ -159,176 +225,314 @@ export class LightraceCallbackHandler extends BaseCallbackHandler {
|
|
|
159
225
|
this.runs.delete(runId);
|
|
160
226
|
}
|
|
161
227
|
}
|
|
162
|
-
//
|
|
163
|
-
async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
228
|
+
// -- Chain callbacks --------------------------------------------------------
|
|
229
|
+
async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, _runType, name) {
|
|
230
|
+
try {
|
|
231
|
+
const resolvedName = name ??
|
|
232
|
+
chain?.name ??
|
|
233
|
+
chain?.id?.[chain.id.length - 1] ??
|
|
234
|
+
"Chain";
|
|
235
|
+
const type = this.isAgent(chain) ? "chain" : "chain";
|
|
236
|
+
const mergedMetadata = { ...metadata, ...(tags?.length ? { tags } : {}) };
|
|
237
|
+
this.registerRun(runId, parentRunId, {
|
|
238
|
+
type,
|
|
239
|
+
name: resolvedName,
|
|
240
|
+
startTime: new Date(),
|
|
241
|
+
input: this.normalizeIO(inputs ?? {}),
|
|
242
|
+
metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
console.warn("[lightrace] Error in handleChainStart:", e);
|
|
247
|
+
}
|
|
176
248
|
}
|
|
177
249
|
async handleChainEnd(outputs, runId, _parentRunId) {
|
|
178
|
-
|
|
250
|
+
try {
|
|
251
|
+
this.endRun(runId, this.normalizeIO(outputs ?? null));
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
console.warn("[lightrace] Error in handleChainEnd:", e);
|
|
255
|
+
}
|
|
179
256
|
}
|
|
180
257
|
async handleChainError(error, runId, _parentRunId) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
258
|
+
try {
|
|
259
|
+
// LangGraph GraphBubbleUp is a control flow error, not an actual error
|
|
260
|
+
if (error?.constructor?.name === "GraphBubbleUp" || error?.name === "GraphBubbleUp") {
|
|
261
|
+
this.endRun(runId, null, "DEFAULT", null);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.endRun(runId, null, "ERROR", error?.message ?? String(error));
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
console.warn("[lightrace] Error in handleChainError:", e);
|
|
185
268
|
}
|
|
186
|
-
this.endRun(runId, null, "ERROR", error.message ?? String(error));
|
|
187
269
|
}
|
|
188
|
-
//
|
|
189
|
-
async handleLLMStart(llm, prompts, runId, parentRunId,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
270
|
+
// -- LLM callbacks ----------------------------------------------------------
|
|
271
|
+
async handleLLMStart(llm, prompts, runId, parentRunId, extraParams, _tags, metadata, name) {
|
|
272
|
+
try {
|
|
273
|
+
const model = this.extractModelName(llm);
|
|
274
|
+
const resolvedName = name ??
|
|
275
|
+
model ??
|
|
276
|
+
llm?.name ??
|
|
277
|
+
llm?.id?.[llm.id.length - 1] ??
|
|
278
|
+
"LLM";
|
|
279
|
+
const modelParameters = this.extractModelParameters(extraParams);
|
|
280
|
+
this.registerRun(runId, parentRunId, {
|
|
281
|
+
type: "generation",
|
|
282
|
+
name: resolvedName,
|
|
283
|
+
startTime: new Date(),
|
|
284
|
+
input: prompts?.length === 1 ? prompts[0] : (prompts ?? []),
|
|
285
|
+
model,
|
|
286
|
+
metadata,
|
|
287
|
+
modelParameters,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
console.warn("[lightrace] Error in handleLLMStart:", e);
|
|
292
|
+
}
|
|
203
293
|
}
|
|
204
|
-
async handleChatModelStart(llm, messages, runId, parentRunId,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
content
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
async handleChatModelStart(llm, messages, runId, parentRunId, extraParams, _tags, metadata, name) {
|
|
295
|
+
try {
|
|
296
|
+
const model = this.extractModelName(llm);
|
|
297
|
+
const resolvedName = name ??
|
|
298
|
+
model ??
|
|
299
|
+
llm?.name ??
|
|
300
|
+
llm?.id?.[llm.id.length - 1] ??
|
|
301
|
+
"ChatModel";
|
|
302
|
+
const modelParameters = this.extractModelParameters(extraParams);
|
|
303
|
+
// Convert messages to a simpler role/content format
|
|
304
|
+
const formattedMessages = (messages ?? []).map((msgGroup) => (msgGroup ?? []).map((msg) => {
|
|
305
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
306
|
+
const msgAny = msg;
|
|
307
|
+
const role = typeof msgAny?._getType === "function"
|
|
308
|
+
? msgAny._getType()
|
|
309
|
+
: (msgAny?.type ?? "unknown");
|
|
310
|
+
return {
|
|
311
|
+
role,
|
|
312
|
+
content: msg?.content,
|
|
313
|
+
...(msg?.name ? { name: msg.name } : {}),
|
|
314
|
+
...(msgAny?.tool_calls?.length ? { tool_calls: msgAny.tool_calls } : {}),
|
|
315
|
+
};
|
|
316
|
+
}));
|
|
317
|
+
this.registerRun(runId, parentRunId, {
|
|
318
|
+
type: "generation",
|
|
319
|
+
name: resolvedName,
|
|
320
|
+
startTime: new Date(),
|
|
321
|
+
input: formattedMessages.length === 1 ? formattedMessages[0] : formattedMessages,
|
|
322
|
+
model,
|
|
323
|
+
metadata,
|
|
324
|
+
modelParameters,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
console.warn("[lightrace] Error in handleChatModelStart:", e);
|
|
329
|
+
}
|
|
224
330
|
}
|
|
225
331
|
async handleLLMEnd(output, runId, _parentRunId) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
332
|
+
try {
|
|
333
|
+
const extra = {};
|
|
334
|
+
const run = this.runs.get(runId);
|
|
335
|
+
// Extract token usage from multiple sources
|
|
336
|
+
const llmUsage = output?.llmOutput?.tokenUsage ?? output?.llmOutput?.usage ?? null;
|
|
337
|
+
// Also check generation-level usage_metadata (e.g. Anthropic, Google)
|
|
338
|
+
const lastGen = output?.generations?.at(-1)?.at(-1);
|
|
339
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
340
|
+
const msgUsage = lastGen?.message?.usage_metadata;
|
|
341
|
+
const usage = llmUsage ?? msgUsage;
|
|
342
|
+
if (usage) {
|
|
343
|
+
const promptTokens = usage.promptTokens ?? usage.prompt_tokens ?? usage.input_tokens ?? undefined;
|
|
344
|
+
const completionTokens = usage.completionTokens ?? usage.completion_tokens ?? usage.output_tokens ?? undefined;
|
|
345
|
+
const totalTokens = usage.totalTokens ?? usage.total_tokens ?? undefined;
|
|
346
|
+
if (promptTokens !== undefined)
|
|
347
|
+
extra.promptTokens = promptTokens;
|
|
348
|
+
if (completionTokens !== undefined)
|
|
349
|
+
extra.completionTokens = completionTokens;
|
|
350
|
+
if (totalTokens !== undefined)
|
|
351
|
+
extra.totalTokens = totalTokens;
|
|
352
|
+
else if (promptTokens !== undefined && completionTokens !== undefined)
|
|
353
|
+
extra.totalTokens = promptTokens + completionTokens;
|
|
354
|
+
}
|
|
355
|
+
// Extract model name from response if not set at start
|
|
356
|
+
if (run && !run.model) {
|
|
357
|
+
const modelFromResponse = output?.llmOutput?.model_name ?? output?.llmOutput?.model ?? undefined;
|
|
358
|
+
if (modelFromResponse) {
|
|
359
|
+
run.model = modelFromResponse;
|
|
360
|
+
// Also update name if it was a default
|
|
361
|
+
if (run.name === "LLM" || run.name === "ChatModel") {
|
|
362
|
+
run.name = modelFromResponse;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Add TTFT if we have it
|
|
367
|
+
const ttftStart = this.completionStartTimes.get(runId);
|
|
368
|
+
if (ttftStart && run) {
|
|
369
|
+
const ttft = ttftStart.getTime() - run.startTime.getTime();
|
|
370
|
+
if (!run.metadata)
|
|
371
|
+
run.metadata = {};
|
|
372
|
+
run.metadata.timeToFirstToken = ttft;
|
|
373
|
+
}
|
|
374
|
+
// Extract output -- handle ChatGeneration (has .message) vs Generation (only .text)
|
|
375
|
+
let outputData;
|
|
376
|
+
const generations = output?.generations;
|
|
377
|
+
if (generations && generations.length > 0) {
|
|
378
|
+
const singleGen = generations.length === 1 && generations[0].length === 1;
|
|
379
|
+
if (singleGen) {
|
|
380
|
+
const gen = generations[0][0];
|
|
381
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
382
|
+
const genAny = gen;
|
|
383
|
+
if (genAny?.message) {
|
|
384
|
+
const msg = genAny.message;
|
|
385
|
+
const role = typeof msg._getType === "function" ? msg._getType() : (msg.type ?? "assistant");
|
|
386
|
+
outputData = {
|
|
387
|
+
role,
|
|
388
|
+
content: msg.content,
|
|
389
|
+
...(msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {}),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
outputData = gen?.text;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
outputData = generations.map((g) => g.map((gg) => {
|
|
398
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
399
|
+
const ggAny = gg;
|
|
400
|
+
if (ggAny?.message) {
|
|
401
|
+
const msg = ggAny.message;
|
|
402
|
+
const role = typeof msg._getType === "function" ? msg._getType() : (msg.type ?? "assistant");
|
|
403
|
+
return {
|
|
404
|
+
role,
|
|
405
|
+
content: msg.content,
|
|
406
|
+
...(msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return gg?.text;
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
outputData = null;
|
|
415
|
+
}
|
|
416
|
+
this.endRun(runId, outputData, "DEFAULT", null, extra);
|
|
417
|
+
this.completionStartTimes.delete(runId);
|
|
254
418
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const ggAny = gg;
|
|
258
|
-
return ggAny.message ?? gg.text;
|
|
259
|
-
}));
|
|
419
|
+
catch (e) {
|
|
420
|
+
console.warn("[lightrace] Error in handleLLMEnd:", e);
|
|
260
421
|
}
|
|
261
|
-
this.endRun(runId, outputText, "DEFAULT", null, extra);
|
|
262
|
-
this.completionStartTimes.delete(runId);
|
|
263
422
|
}
|
|
264
423
|
async handleLLMNewToken(_token, _idx, runId) {
|
|
265
|
-
|
|
266
|
-
this.completionStartTimes.
|
|
424
|
+
try {
|
|
425
|
+
if (!this.completionStartTimes.has(runId)) {
|
|
426
|
+
this.completionStartTimes.set(runId, new Date());
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
console.warn("[lightrace] Error in handleLLMNewToken:", e);
|
|
267
431
|
}
|
|
268
432
|
}
|
|
269
433
|
async handleLLMError(error, runId, _parentRunId) {
|
|
270
|
-
|
|
271
|
-
|
|
434
|
+
try {
|
|
435
|
+
this.endRun(runId, null, "ERROR", error?.message ?? String(error));
|
|
436
|
+
this.completionStartTimes.delete(runId);
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
console.warn("[lightrace] Error in handleLLMError:", e);
|
|
440
|
+
}
|
|
272
441
|
}
|
|
273
|
-
//
|
|
274
|
-
async handleToolStart(tool, input, runId, parentRunId, _tags, metadata) {
|
|
275
|
-
const name = tool.name ??
|
|
276
|
-
tool.id?.[tool.id.length - 1] ??
|
|
277
|
-
"Tool";
|
|
278
|
-
// Try to parse input as JSON
|
|
279
|
-
let parsedInput = input;
|
|
442
|
+
// -- Tool callbacks ---------------------------------------------------------
|
|
443
|
+
async handleToolStart(tool, input, runId, parentRunId, _tags, metadata, name) {
|
|
280
444
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
445
|
+
const resolvedName = name ??
|
|
446
|
+
tool?.name ??
|
|
447
|
+
tool?.id?.[tool.id.length - 1] ??
|
|
448
|
+
"Tool";
|
|
449
|
+
// Try to parse input as JSON
|
|
450
|
+
let parsedInput = input;
|
|
451
|
+
if (typeof input === "string") {
|
|
452
|
+
try {
|
|
453
|
+
parsedInput = JSON.parse(input);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// keep as string
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this.registerRun(runId, parentRunId, {
|
|
460
|
+
type: "tool",
|
|
461
|
+
name: resolvedName,
|
|
462
|
+
startTime: new Date(),
|
|
463
|
+
input: parsedInput,
|
|
464
|
+
metadata,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch (e) {
|
|
468
|
+
console.warn("[lightrace] Error in handleToolStart:", e);
|
|
469
|
+
}
|
|
293
470
|
}
|
|
294
471
|
async handleToolEnd(output, runId, _parentRunId) {
|
|
295
|
-
// Try to parse output as JSON
|
|
296
|
-
let parsedOutput = output;
|
|
297
472
|
try {
|
|
298
|
-
|
|
473
|
+
// Try to parse output as JSON
|
|
474
|
+
let parsedOutput = output;
|
|
475
|
+
if (typeof output === "string") {
|
|
476
|
+
try {
|
|
477
|
+
parsedOutput = JSON.parse(output);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// keep as string
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.endRun(runId, parsedOutput);
|
|
299
484
|
}
|
|
300
|
-
catch {
|
|
301
|
-
|
|
485
|
+
catch (e) {
|
|
486
|
+
console.warn("[lightrace] Error in handleToolEnd:", e);
|
|
302
487
|
}
|
|
303
|
-
this.endRun(runId, parsedOutput);
|
|
304
488
|
}
|
|
305
489
|
async handleToolError(error, runId, _parentRunId) {
|
|
306
|
-
|
|
490
|
+
try {
|
|
491
|
+
this.endRun(runId, null, "ERROR", error?.message ?? String(error));
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
console.warn("[lightrace] Error in handleToolError:", e);
|
|
495
|
+
}
|
|
307
496
|
}
|
|
308
|
-
//
|
|
497
|
+
// -- Retriever callbacks ----------------------------------------------------
|
|
309
498
|
async handleRetrieverStart(retriever, query, runId, parentRunId, _tags, metadata) {
|
|
310
|
-
|
|
311
|
-
retriever
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
499
|
+
try {
|
|
500
|
+
const name = retriever?.name ??
|
|
501
|
+
retriever?.id?.[retriever.id.length - 1] ??
|
|
502
|
+
"Retriever";
|
|
503
|
+
this.registerRun(runId, parentRunId, {
|
|
504
|
+
type: "span",
|
|
505
|
+
name,
|
|
506
|
+
startTime: new Date(),
|
|
507
|
+
input: query,
|
|
508
|
+
metadata,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
console.warn("[lightrace] Error in handleRetrieverStart:", e);
|
|
513
|
+
}
|
|
320
514
|
}
|
|
321
515
|
async handleRetrieverEnd(documents, runId, _parentRunId) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
516
|
+
try {
|
|
517
|
+
const output = (documents ?? []).map((doc) => ({
|
|
518
|
+
pageContent: doc?.pageContent,
|
|
519
|
+
metadata: doc?.metadata,
|
|
520
|
+
}));
|
|
521
|
+
this.endRun(runId, output);
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
console.warn("[lightrace] Error in handleRetrieverEnd:", e);
|
|
525
|
+
}
|
|
327
526
|
}
|
|
328
527
|
async handleRetrieverError(error, runId, _parentRunId) {
|
|
329
|
-
|
|
528
|
+
try {
|
|
529
|
+
this.endRun(runId, null, "ERROR", error?.message ?? String(error));
|
|
530
|
+
}
|
|
531
|
+
catch (e) {
|
|
532
|
+
console.warn("[lightrace] Error in handleRetrieverError:", e);
|
|
533
|
+
}
|
|
330
534
|
}
|
|
331
|
-
//
|
|
535
|
+
// -- Accessors --------------------------------------------------------------
|
|
332
536
|
/** Get the current active trace ID (null if no run is active). */
|
|
333
537
|
get traceId() {
|
|
334
538
|
return this._traceId;
|