@mastra/braintrust 1.0.0-beta.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +358 -0
- package/README.md +47 -5
- package/dist/formatter.d.ts +99 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/index.cjs +388 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +389 -204
- package/dist/index.js.map +1 -1
- package/dist/thread-reconstruction.d.ts +47 -0
- package/dist/thread-reconstruction.d.ts.map +1 -0
- package/dist/tracing.d.ts +69 -25
- package/dist/tracing.d.ts.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,10 +1,140 @@
|
|
|
1
1
|
import { SpanType } from '@mastra/core/observability';
|
|
2
2
|
import { omitKeys } from '@mastra/core/utils';
|
|
3
|
-
import {
|
|
3
|
+
import { TrackingExporter } from '@mastra/observability';
|
|
4
4
|
import { initLogger, currentSpan } from 'braintrust';
|
|
5
5
|
|
|
6
6
|
// src/tracing.ts
|
|
7
7
|
|
|
8
|
+
// src/formatter.ts
|
|
9
|
+
function removeNullish(obj) {
|
|
10
|
+
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
|
|
11
|
+
}
|
|
12
|
+
function convertContentPart(part) {
|
|
13
|
+
if (!part || typeof part !== "object") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
switch (part.type) {
|
|
17
|
+
case "text":
|
|
18
|
+
return part.text || null;
|
|
19
|
+
case "image":
|
|
20
|
+
return "[image]";
|
|
21
|
+
case "file": {
|
|
22
|
+
const filePart = part;
|
|
23
|
+
if (filePart.filename || filePart.name) {
|
|
24
|
+
return `[file: ${filePart.filename || filePart.name}]`;
|
|
25
|
+
}
|
|
26
|
+
return "[file]";
|
|
27
|
+
}
|
|
28
|
+
case "reasoning": {
|
|
29
|
+
const reasoningPart = part;
|
|
30
|
+
if (typeof reasoningPart.text === "string" && reasoningPart.text.length > 0) {
|
|
31
|
+
return `[reasoning: ${reasoningPart.text.substring(0, 100)}${reasoningPart.text.length > 100 ? "..." : ""}]`;
|
|
32
|
+
}
|
|
33
|
+
return "[reasoning]";
|
|
34
|
+
}
|
|
35
|
+
case "tool-call":
|
|
36
|
+
return null;
|
|
37
|
+
case "tool-result":
|
|
38
|
+
return null;
|
|
39
|
+
default: {
|
|
40
|
+
const unknownPart = part;
|
|
41
|
+
if (typeof unknownPart.text === "string") {
|
|
42
|
+
return unknownPart.text;
|
|
43
|
+
}
|
|
44
|
+
if (typeof unknownPart.content === "string") {
|
|
45
|
+
return unknownPart.content;
|
|
46
|
+
}
|
|
47
|
+
return `[${unknownPart.type || "unknown"}]`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function serializeToolResult(resultData) {
|
|
52
|
+
if (typeof resultData === "string") {
|
|
53
|
+
return resultData;
|
|
54
|
+
}
|
|
55
|
+
if (resultData && typeof resultData === "object" && "value" in resultData) {
|
|
56
|
+
const valueData = resultData.value;
|
|
57
|
+
return typeof valueData === "string" ? valueData : JSON.stringify(valueData);
|
|
58
|
+
}
|
|
59
|
+
if (resultData === void 0 || resultData === null) {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return JSON.stringify(resultData);
|
|
64
|
+
} catch {
|
|
65
|
+
return "[unserializable result]";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function convertAISDKMessage(message) {
|
|
69
|
+
if (!message || typeof message !== "object") {
|
|
70
|
+
return message;
|
|
71
|
+
}
|
|
72
|
+
const { role, content, ...rest } = message;
|
|
73
|
+
if (typeof content === "string") {
|
|
74
|
+
return message;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(content)) {
|
|
77
|
+
if (content.length === 0) {
|
|
78
|
+
return { role, content: "", ...rest };
|
|
79
|
+
}
|
|
80
|
+
if (role === "user" || role === "system") {
|
|
81
|
+
const contentParts = content.map((part) => convertContentPart(part)).filter(Boolean);
|
|
82
|
+
return {
|
|
83
|
+
role,
|
|
84
|
+
content: contentParts.length > 0 ? contentParts.join("\n") : "",
|
|
85
|
+
...rest
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (role === "assistant") {
|
|
89
|
+
const contentParts = content.filter((part) => part?.type !== "tool-call").map((part) => convertContentPart(part)).filter(Boolean);
|
|
90
|
+
const toolCallParts = content.filter((part) => part?.type === "tool-call");
|
|
91
|
+
const result = {
|
|
92
|
+
role,
|
|
93
|
+
content: contentParts.length > 0 ? contentParts.join("\n") : "",
|
|
94
|
+
...rest
|
|
95
|
+
};
|
|
96
|
+
if (toolCallParts.length > 0) {
|
|
97
|
+
result.tool_calls = toolCallParts.map((tc) => {
|
|
98
|
+
const toolCall = tc;
|
|
99
|
+
const toolCallId = toolCall.toolCallId;
|
|
100
|
+
const toolName = toolCall.toolName;
|
|
101
|
+
const args = toolCall.args ?? toolCall.input;
|
|
102
|
+
let argsString;
|
|
103
|
+
if (typeof args === "string") {
|
|
104
|
+
argsString = args;
|
|
105
|
+
} else if (args !== void 0 && args !== null) {
|
|
106
|
+
argsString = JSON.stringify(args);
|
|
107
|
+
} else {
|
|
108
|
+
argsString = "{}";
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id: toolCallId,
|
|
112
|
+
type: "function",
|
|
113
|
+
function: {
|
|
114
|
+
name: toolName,
|
|
115
|
+
arguments: argsString
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
if (role === "tool") {
|
|
123
|
+
const toolResult = content.find((part) => part?.type === "tool-result");
|
|
124
|
+
if (toolResult) {
|
|
125
|
+
const resultData = toolResult.output ?? toolResult.result;
|
|
126
|
+
const resultContent = serializeToolResult(resultData);
|
|
127
|
+
return {
|
|
128
|
+
role: "tool",
|
|
129
|
+
content: resultContent,
|
|
130
|
+
tool_call_id: toolResult.toolCallId
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return message;
|
|
136
|
+
}
|
|
137
|
+
|
|
8
138
|
// src/metrics.ts
|
|
9
139
|
function formatUsageMetrics(usage) {
|
|
10
140
|
const metrics = {};
|
|
@@ -29,8 +159,61 @@ function formatUsageMetrics(usage) {
|
|
|
29
159
|
return metrics;
|
|
30
160
|
}
|
|
31
161
|
|
|
162
|
+
// src/thread-reconstruction.ts
|
|
163
|
+
function reconstructThreadOutput(threadData, originalOutput) {
|
|
164
|
+
const messages = [];
|
|
165
|
+
const sortedSteps = [...threadData].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
166
|
+
for (const step of sortedSteps) {
|
|
167
|
+
const sortedToolCalls = step.toolCalls ? [...step.toolCalls].sort((a, b) => {
|
|
168
|
+
if (!a.startTime || !b.startTime) return 0;
|
|
169
|
+
return a.startTime.getTime() - b.startTime.getTime();
|
|
170
|
+
}) : [];
|
|
171
|
+
if (sortedToolCalls.length > 0) {
|
|
172
|
+
messages.push({
|
|
173
|
+
role: "assistant",
|
|
174
|
+
content: step.text || "",
|
|
175
|
+
tool_calls: sortedToolCalls.map((tc) => {
|
|
176
|
+
const cleanArgs = tc.args && typeof tc.args === "object" && !Array.isArray(tc.args) ? removeNullish(tc.args) : tc.args;
|
|
177
|
+
return {
|
|
178
|
+
id: tc.toolCallId,
|
|
179
|
+
type: "function",
|
|
180
|
+
function: {
|
|
181
|
+
name: tc.toolName,
|
|
182
|
+
arguments: typeof cleanArgs === "string" ? cleanArgs : JSON.stringify(cleanArgs)
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
for (const tc of sortedToolCalls) {
|
|
188
|
+
if (tc.result !== void 0) {
|
|
189
|
+
messages.push({
|
|
190
|
+
role: "tool",
|
|
191
|
+
content: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result),
|
|
192
|
+
tool_call_id: tc.toolCallId
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else if (step.text) {
|
|
197
|
+
messages.push({
|
|
198
|
+
role: "assistant",
|
|
199
|
+
content: step.text
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (messages.length > 0) {
|
|
204
|
+
const lastMessage = messages[messages.length - 1];
|
|
205
|
+
const originalText = originalOutput?.text;
|
|
206
|
+
if (originalText && lastMessage.role === "tool") {
|
|
207
|
+
messages.push({
|
|
208
|
+
role: "assistant",
|
|
209
|
+
content: originalText
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return messages;
|
|
214
|
+
}
|
|
215
|
+
|
|
32
216
|
// src/tracing.ts
|
|
33
|
-
var MASTRA_TRACE_ID_METADATA_KEY = "mastra-trace-id";
|
|
34
217
|
var DEFAULT_SPAN_TYPE = "task";
|
|
35
218
|
var SPAN_TYPE_EXCEPTIONS = {
|
|
36
219
|
[SpanType.MODEL_GENERATION]: "llm",
|
|
@@ -42,246 +225,250 @@ var SPAN_TYPE_EXCEPTIONS = {
|
|
|
42
225
|
function mapSpanType(spanType) {
|
|
43
226
|
return SPAN_TYPE_EXCEPTIONS[spanType] ?? DEFAULT_SPAN_TYPE;
|
|
44
227
|
}
|
|
45
|
-
var BraintrustExporter = class extends
|
|
228
|
+
var BraintrustExporter = class extends TrackingExporter {
|
|
46
229
|
name = "braintrust";
|
|
47
|
-
traceMap = /* @__PURE__ */ new Map();
|
|
48
|
-
config;
|
|
49
230
|
// Flags and logger for context-aware mode
|
|
50
|
-
useProvidedLogger;
|
|
51
|
-
providedLogger;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
231
|
+
#useProvidedLogger;
|
|
232
|
+
#providedLogger;
|
|
233
|
+
#localLogger;
|
|
234
|
+
constructor(config = {}) {
|
|
235
|
+
const resolvedApiKey = config.apiKey ?? process.env.BRAINTRUST_API_KEY;
|
|
236
|
+
const resolvedEndpoint = config.endpoint ?? process.env.BRAINTRUST_ENDPOINT;
|
|
237
|
+
super({
|
|
238
|
+
...config,
|
|
239
|
+
apiKey: resolvedApiKey,
|
|
240
|
+
endpoint: resolvedEndpoint
|
|
241
|
+
});
|
|
242
|
+
this.#useProvidedLogger = !!config.braintrustLogger;
|
|
243
|
+
if (this.#useProvidedLogger) {
|
|
244
|
+
this.#providedLogger = config.braintrustLogger;
|
|
58
245
|
} else {
|
|
59
|
-
if (!config.apiKey) {
|
|
60
|
-
this.setDisabled(
|
|
61
|
-
|
|
62
|
-
|
|
246
|
+
if (!this.config.apiKey) {
|
|
247
|
+
this.setDisabled(
|
|
248
|
+
`Missing required API key. Set BRAINTRUST_API_KEY environment variable or pass apiKey in config.`
|
|
249
|
+
);
|
|
63
250
|
return;
|
|
64
251
|
}
|
|
65
|
-
this
|
|
66
|
-
this.config = config;
|
|
252
|
+
this.#localLogger = void 0;
|
|
67
253
|
}
|
|
68
254
|
}
|
|
69
|
-
async
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
return;
|
|
255
|
+
async getLocalLogger() {
|
|
256
|
+
if (this.#localLogger) {
|
|
257
|
+
return this.#localLogger;
|
|
73
258
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
259
|
+
try {
|
|
260
|
+
const logger = await initLogger({
|
|
261
|
+
projectName: this.config.projectName ?? "mastra-tracing",
|
|
262
|
+
apiKey: this.config.apiKey,
|
|
263
|
+
appUrl: this.config.endpoint,
|
|
264
|
+
...this.config.tuningParameters
|
|
265
|
+
});
|
|
266
|
+
this.#localLogger = logger;
|
|
267
|
+
return logger;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.logger.error("Braintrust exporter: Failed to initialize logger", { error: err });
|
|
270
|
+
this.setDisabled("Failed to initialize Braintrust logger");
|
|
84
271
|
}
|
|
85
272
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (this.useProvidedLogger) {
|
|
89
|
-
await this.initLoggerOrUseContext(span);
|
|
90
|
-
} else {
|
|
91
|
-
await this.initLoggerPerTrace(span);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const method = "handleSpanStarted";
|
|
95
|
-
const spanData = this.getSpanData({ span, method });
|
|
96
|
-
if (!spanData) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (!span.isEvent) {
|
|
100
|
-
spanData.activeIds.add(span.id);
|
|
101
|
-
}
|
|
102
|
-
const braintrustParent = this.getBraintrustParent({ spanData, span, method });
|
|
103
|
-
if (!braintrustParent) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
273
|
+
startSpan(args) {
|
|
274
|
+
const { parent, span } = args;
|
|
106
275
|
const payload = this.buildSpanPayload(span);
|
|
107
|
-
const braintrustSpan =
|
|
276
|
+
const braintrustSpan = parent.startSpan({
|
|
108
277
|
spanId: span.id,
|
|
109
278
|
name: span.name,
|
|
110
279
|
type: mapSpanType(span.type),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
...span.isRootSpan && span.tags?.length ? { tags: span.tags } : {}
|
|
280
|
+
startTime: span.startTime.getTime() / 1e3,
|
|
281
|
+
event: {
|
|
282
|
+
id: span.id,
|
|
283
|
+
// Use Mastra span ID as Braintrust row ID for logFeedback() compatibility
|
|
284
|
+
...payload
|
|
285
|
+
}
|
|
118
286
|
});
|
|
119
|
-
|
|
287
|
+
const isModelGeneration = span.type === SpanType.MODEL_GENERATION;
|
|
288
|
+
return {
|
|
289
|
+
span: braintrustSpan,
|
|
290
|
+
spanType: span.type,
|
|
291
|
+
threadData: isModelGeneration ? [] : void 0,
|
|
292
|
+
pendingToolResults: isModelGeneration ? /* @__PURE__ */ new Map() : void 0
|
|
293
|
+
};
|
|
120
294
|
}
|
|
121
|
-
async
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
const braintrustSpan = spanData.spans.get(span.id);
|
|
128
|
-
if (!braintrustSpan) {
|
|
129
|
-
this.logger.warn("Braintrust exporter: No Braintrust span found for span update/end", {
|
|
130
|
-
traceId: span.traceId,
|
|
131
|
-
spanId: span.id,
|
|
132
|
-
spanName: span.name,
|
|
133
|
-
spanType: span.type,
|
|
134
|
-
isRootSpan: span.isRootSpan,
|
|
135
|
-
parentSpanId: span.parentSpanId,
|
|
136
|
-
method
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
braintrustSpan.log(this.buildSpanPayload(span));
|
|
141
|
-
if (isEnd) {
|
|
142
|
-
if (span.endTime) {
|
|
143
|
-
braintrustSpan.end({ endTime: span.endTime.getTime() / 1e3 });
|
|
295
|
+
async _buildRoot(_args) {
|
|
296
|
+
if (this.#useProvidedLogger) {
|
|
297
|
+
const externalSpan = currentSpan();
|
|
298
|
+
if (externalSpan && externalSpan.id) {
|
|
299
|
+
return externalSpan;
|
|
144
300
|
} else {
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
if (!span.isEvent) {
|
|
148
|
-
spanData.activeIds.delete(span.id);
|
|
149
|
-
}
|
|
150
|
-
if (spanData.activeIds.size === 0 && !spanData.isExternal) {
|
|
151
|
-
this.traceMap.delete(span.traceId);
|
|
301
|
+
return this.#providedLogger;
|
|
152
302
|
}
|
|
303
|
+
} else {
|
|
304
|
+
return this.getLocalLogger();
|
|
153
305
|
}
|
|
154
306
|
}
|
|
155
|
-
async
|
|
307
|
+
async _buildSpan(args) {
|
|
308
|
+
const { span, traceData } = args;
|
|
156
309
|
if (span.isRootSpan) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await this.initLoggerPerTrace(span);
|
|
310
|
+
const root = traceData.getRoot();
|
|
311
|
+
if (root) {
|
|
312
|
+
return this.startSpan({ parent: root, span });
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
const parent = traceData.getParent(args);
|
|
316
|
+
if (parent) {
|
|
317
|
+
const parentSpan = "span" in parent ? parent.span : parent;
|
|
318
|
+
return this.startSpan({ parent: parentSpan, span });
|
|
167
319
|
}
|
|
168
320
|
}
|
|
169
|
-
|
|
170
|
-
|
|
321
|
+
}
|
|
322
|
+
async _buildEvent(args) {
|
|
323
|
+
const spanData = await this._buildSpan(args);
|
|
171
324
|
if (!spanData) {
|
|
172
325
|
return;
|
|
173
326
|
}
|
|
174
|
-
|
|
175
|
-
|
|
327
|
+
spanData.span.end({ endTime: args.span.startTime.getTime() / 1e3 });
|
|
328
|
+
return spanData.span;
|
|
329
|
+
}
|
|
330
|
+
async _updateSpan(args) {
|
|
331
|
+
const { span, traceData } = args;
|
|
332
|
+
const spanData = traceData.getSpan({ spanId: span.id });
|
|
333
|
+
if (!spanData) {
|
|
176
334
|
return;
|
|
177
335
|
}
|
|
178
|
-
|
|
179
|
-
const braintrustSpan = braintrustParent.startSpan({
|
|
180
|
-
spanId: span.id,
|
|
181
|
-
name: span.name,
|
|
182
|
-
type: mapSpanType(span.type),
|
|
183
|
-
startTime: span.startTime.getTime() / 1e3,
|
|
184
|
-
...payload
|
|
185
|
-
});
|
|
186
|
-
braintrustSpan.end({ endTime: span.startTime.getTime() / 1e3 });
|
|
336
|
+
spanData.span.log(this.buildSpanPayload(span, false));
|
|
187
337
|
}
|
|
188
|
-
|
|
189
|
-
const {
|
|
190
|
-
|
|
191
|
-
|
|
338
|
+
async _finishSpan(args) {
|
|
339
|
+
const { span, traceData } = args;
|
|
340
|
+
const spanData = traceData.getSpan({ spanId: span.id });
|
|
341
|
+
if (!spanData) {
|
|
192
342
|
return;
|
|
193
343
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
344
|
+
if (span.type === SpanType.MODEL_STEP) {
|
|
345
|
+
this.accumulateModelStepData(span, traceData);
|
|
346
|
+
} else if (span.type === SpanType.TOOL_CALL) {
|
|
347
|
+
this.accumulateToolCallResult(span, traceData);
|
|
348
|
+
}
|
|
349
|
+
const payload = span.type === SpanType.MODEL_GENERATION ? this.buildModelGenerationPayload(span, spanData) : this.buildSpanPayload(span, false);
|
|
350
|
+
spanData.span.log(payload);
|
|
351
|
+
if (span.endTime) {
|
|
352
|
+
spanData.span.end({ endTime: span.endTime.getTime() / 1e3 });
|
|
353
|
+
} else {
|
|
354
|
+
spanData.span.end();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async _abortSpan(args) {
|
|
358
|
+
const { span: spanData, reason } = args;
|
|
359
|
+
spanData.span.log({
|
|
360
|
+
error: reason.message,
|
|
361
|
+
metadata: { errorDetails: reason }
|
|
199
362
|
});
|
|
363
|
+
spanData.span.end();
|
|
200
364
|
}
|
|
365
|
+
// ==============================================================================
|
|
366
|
+
// Thread view reconstruction helpers
|
|
367
|
+
// ==============================================================================
|
|
201
368
|
/**
|
|
202
|
-
*
|
|
369
|
+
* Walk up the tree to find the MODEL_GENERATION ancestor span.
|
|
370
|
+
* Returns the BraintrustSpanData if found, undefined otherwise.
|
|
203
371
|
*/
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
...this.config.tuningParameters
|
|
215
|
-
});
|
|
216
|
-
this.initTraceMap({ logger: loggerInstance, isExternal: false, traceId: span.traceId });
|
|
217
|
-
} catch (err) {
|
|
218
|
-
this.logger.error("Braintrust exporter: Failed to initialize logger", { error: err, traceId: span.traceId });
|
|
219
|
-
this.setDisabled("Failed to initialize Braintrust logger");
|
|
372
|
+
findModelGenerationAncestor(spanId, traceData) {
|
|
373
|
+
let currentId = spanId;
|
|
374
|
+
while (currentId) {
|
|
375
|
+
const parentId = traceData.getParentId({ spanId: currentId });
|
|
376
|
+
if (!parentId) return void 0;
|
|
377
|
+
const parentSpanData = traceData.getSpan({ spanId: parentId });
|
|
378
|
+
if (parentSpanData?.spanType === SpanType.MODEL_GENERATION) {
|
|
379
|
+
return parentSpanData;
|
|
380
|
+
}
|
|
381
|
+
currentId = parentId;
|
|
220
382
|
}
|
|
383
|
+
return void 0;
|
|
221
384
|
}
|
|
222
385
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* Otherwise, uses the provided logger instance.
|
|
386
|
+
* Accumulate MODEL_STEP data to the parent MODEL_GENERATION's threadData.
|
|
387
|
+
* Called when a MODEL_STEP span ends.
|
|
226
388
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
389
|
+
accumulateModelStepData(span, traceData) {
|
|
390
|
+
const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
|
|
391
|
+
if (!modelGenSpanData?.threadData) {
|
|
230
392
|
return;
|
|
231
393
|
}
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
394
|
+
const output = span.output;
|
|
395
|
+
const attributes = span.attributes;
|
|
396
|
+
const stepData = {
|
|
397
|
+
stepSpanId: span.id,
|
|
398
|
+
stepIndex: attributes?.stepIndex ?? 0,
|
|
399
|
+
text: output?.text,
|
|
400
|
+
toolCalls: output?.toolCalls?.map((tc) => ({
|
|
401
|
+
toolCallId: tc.toolCallId,
|
|
402
|
+
toolName: tc.toolName,
|
|
403
|
+
args: tc.args
|
|
404
|
+
}))
|
|
405
|
+
};
|
|
406
|
+
modelGenSpanData.threadData.push(stepData);
|
|
238
407
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Store TOOL_CALL result in parent MODEL_GENERATION's pendingToolResults.
|
|
410
|
+
* Called when a TOOL_CALL span ends.
|
|
411
|
+
* Results are merged into threadData when MODEL_GENERATION ends.
|
|
412
|
+
*/
|
|
413
|
+
accumulateToolCallResult(span, traceData) {
|
|
414
|
+
const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
|
|
415
|
+
if (!modelGenSpanData?.pendingToolResults) {
|
|
416
|
+
return;
|
|
243
417
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
418
|
+
const input = span.input;
|
|
419
|
+
const toolCallId = input?.toolCallId;
|
|
420
|
+
if (!toolCallId) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
modelGenSpanData.pendingToolResults.set(toolCallId, {
|
|
424
|
+
result: span.output,
|
|
425
|
+
startTime: span.startTime
|
|
252
426
|
});
|
|
253
427
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Build the payload for MODEL_GENERATION span, reconstructing output from threadData if available.
|
|
430
|
+
*/
|
|
431
|
+
buildModelGenerationPayload(span, spanData) {
|
|
432
|
+
const basePayload = this.buildSpanPayload(span, false);
|
|
433
|
+
const threadData = spanData.threadData;
|
|
434
|
+
if (!threadData || threadData.length === 0) {
|
|
435
|
+
return basePayload;
|
|
259
436
|
}
|
|
260
|
-
if (spanData.
|
|
261
|
-
|
|
437
|
+
if (spanData.pendingToolResults && spanData.pendingToolResults.size > 0) {
|
|
438
|
+
for (const step of threadData) {
|
|
439
|
+
if (step.toolCalls) {
|
|
440
|
+
for (const toolCall of step.toolCalls) {
|
|
441
|
+
const pendingResult = spanData.pendingToolResults.get(toolCall.toolCallId);
|
|
442
|
+
if (pendingResult) {
|
|
443
|
+
toolCall.result = pendingResult.result;
|
|
444
|
+
toolCall.startTime = pendingResult.startTime;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
262
449
|
}
|
|
263
|
-
|
|
264
|
-
|
|
450
|
+
const hasToolCalls = threadData.some((step) => step.toolCalls && step.toolCalls.length > 0);
|
|
451
|
+
if (!hasToolCalls) {
|
|
452
|
+
return basePayload;
|
|
265
453
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
isRootSpan: span.isRootSpan,
|
|
272
|
-
parentSpanId: span.parentSpanId,
|
|
273
|
-
method
|
|
274
|
-
});
|
|
454
|
+
const reconstructedOutput = reconstructThreadOutput(threadData, span.output);
|
|
455
|
+
return {
|
|
456
|
+
...basePayload,
|
|
457
|
+
output: reconstructedOutput
|
|
458
|
+
};
|
|
275
459
|
}
|
|
276
460
|
/**
|
|
277
461
|
* Transforms MODEL_GENERATION input to Braintrust Thread view format.
|
|
462
|
+
* Converts AI SDK messages (v4/v5) to OpenAI Chat Completion format, which Braintrust requires
|
|
463
|
+
* for proper rendering of threads (fixes #11023).
|
|
278
464
|
*/
|
|
279
465
|
transformInput(input, spanType) {
|
|
280
466
|
if (spanType === SpanType.MODEL_GENERATION) {
|
|
281
|
-
if (
|
|
282
|
-
return input.
|
|
283
|
-
}
|
|
284
|
-
|
|
467
|
+
if (Array.isArray(input)) {
|
|
468
|
+
return input.map((msg) => convertAISDKMessage(msg));
|
|
469
|
+
}
|
|
470
|
+
if (input && typeof input === "object" && "messages" in input && Array.isArray(input.messages)) {
|
|
471
|
+
return input.messages.map((msg) => convertAISDKMessage(msg));
|
|
285
472
|
}
|
|
286
473
|
}
|
|
287
474
|
return input;
|
|
@@ -291,12 +478,15 @@ var BraintrustExporter = class extends BaseExporter {
|
|
|
291
478
|
*/
|
|
292
479
|
transformOutput(output, spanType) {
|
|
293
480
|
if (spanType === SpanType.MODEL_GENERATION) {
|
|
481
|
+
if (!output || typeof output !== "object") {
|
|
482
|
+
return output;
|
|
483
|
+
}
|
|
294
484
|
const { text, ...rest } = output;
|
|
295
|
-
return { role: "assistant", content: text, ...rest };
|
|
485
|
+
return { role: "assistant", content: text, ...removeNullish(rest) };
|
|
296
486
|
}
|
|
297
487
|
return output;
|
|
298
488
|
}
|
|
299
|
-
buildSpanPayload(span) {
|
|
489
|
+
buildSpanPayload(span, isCreate = true) {
|
|
300
490
|
const payload = {};
|
|
301
491
|
if (span.input !== void 0) {
|
|
302
492
|
payload.input = this.transformInput(span.input, span.type);
|
|
@@ -304,11 +494,17 @@ var BraintrustExporter = class extends BaseExporter {
|
|
|
304
494
|
if (span.output !== void 0) {
|
|
305
495
|
payload.output = this.transformOutput(span.output, span.type);
|
|
306
496
|
}
|
|
497
|
+
if (isCreate && span.isRootSpan && span.tags?.length) {
|
|
498
|
+
payload.tags = span.tags;
|
|
499
|
+
}
|
|
307
500
|
payload.metrics = {};
|
|
308
501
|
payload.metadata = {
|
|
309
|
-
|
|
310
|
-
|
|
502
|
+
...span.metadata,
|
|
503
|
+
spanType: span.type
|
|
311
504
|
};
|
|
505
|
+
if (isCreate) {
|
|
506
|
+
payload.metadata["mastra-trace-id"] = span.traceId;
|
|
507
|
+
}
|
|
312
508
|
const attributes = span.attributes ?? {};
|
|
313
509
|
if (span.type === SpanType.MODEL_GENERATION) {
|
|
314
510
|
const modelAttr = attributes;
|
|
@@ -343,20 +539,9 @@ var BraintrustExporter = class extends BaseExporter {
|
|
|
343
539
|
if (Object.keys(payload.metrics).length === 0) {
|
|
344
540
|
delete payload.metrics;
|
|
345
541
|
}
|
|
542
|
+
payload.metadata = removeNullish(payload.metadata);
|
|
346
543
|
return payload;
|
|
347
544
|
}
|
|
348
|
-
async shutdown() {
|
|
349
|
-
if (!this.config) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
for (const [_traceId, spanData] of this.traceMap) {
|
|
353
|
-
for (const [_spanId, span] of spanData.spans) {
|
|
354
|
-
span.end();
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
this.traceMap.clear();
|
|
358
|
-
await super.shutdown();
|
|
359
|
-
}
|
|
360
545
|
};
|
|
361
546
|
|
|
362
547
|
export { BraintrustExporter };
|