@mastra/braintrust 1.0.0-beta.9 → 1.0.1-alpha.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/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 { BaseExporter } from '@mastra/observability';
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 BaseExporter {
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
- constructor(config) {
53
- super(config);
54
- if (config.braintrustLogger) {
55
- this.useProvidedLogger = true;
56
- this.providedLogger = config.braintrustLogger;
57
- this.config = config;
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(`Missing required credentials (apiKey: ${!!config.apiKey})`);
61
- this.config = null;
62
- this.useProvidedLogger = false;
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.useProvidedLogger = false;
66
- this.config = config;
252
+ this.#localLogger = void 0;
67
253
  }
68
254
  }
69
- async _exportTracingEvent(event) {
70
- if (event.exportedSpan.isEvent) {
71
- await this.handleEventSpan(event.exportedSpan);
72
- return;
255
+ async getLocalLogger() {
256
+ if (this.#localLogger) {
257
+ return this.#localLogger;
73
258
  }
74
- switch (event.type) {
75
- case "span_started":
76
- await this.handleSpanStarted(event.exportedSpan);
77
- break;
78
- case "span_updated":
79
- await this.handleSpanUpdateOrEnd(event.exportedSpan, false);
80
- break;
81
- case "span_ended":
82
- await this.handleSpanUpdateOrEnd(event.exportedSpan, true);
83
- break;
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
- async handleSpanStarted(span) {
87
- if (span.isRootSpan) {
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 = braintrustParent.startSpan({
276
+ const braintrustSpan = parent.startSpan({
108
277
  spanId: span.id,
109
278
  name: span.name,
110
279
  type: mapSpanType(span.type),
111
- ...payload
112
- });
113
- braintrustSpan.log({
114
- metadata: {
115
- [MASTRA_TRACE_ID_METADATA_KEY]: span.traceId
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
- spanData.spans.set(span.id, braintrustSpan);
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 handleSpanUpdateOrEnd(span, isEnd) {
122
- const method = isEnd ? "handleSpanEnd" : "handleSpanUpdate";
123
- const spanData = this.getSpanData({ span, method });
124
- if (!spanData) {
125
- return;
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
- braintrustSpan.end();
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 handleEventSpan(span) {
307
+ async _buildSpan(args) {
308
+ const { span, traceData } = args;
156
309
  if (span.isRootSpan) {
157
- this.logger.debug("Braintrust exporter: Creating logger for event", {
158
- traceId: span.traceId,
159
- spanId: span.id,
160
- spanName: span.name,
161
- method: "handleEventSpan"
162
- });
163
- if (this.useProvidedLogger) {
164
- await this.initLoggerOrUseContext(span);
165
- } else {
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
- const method = "handleEventSpan";
170
- const spanData = this.getSpanData({ span, method });
321
+ }
322
+ async _buildEvent(args) {
323
+ const spanData = await this._buildSpan(args);
171
324
  if (!spanData) {
172
325
  return;
173
326
  }
174
- const braintrustParent = this.getBraintrustParent({ spanData, span, method });
175
- if (!braintrustParent) {
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
- const payload = this.buildSpanPayload(span);
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
- initTraceMap(params) {
189
- const { traceId, isExternal, logger } = params;
190
- if (this.traceMap.has(traceId)) {
191
- this.logger.debug("Braintrust exporter: Reusing existing trace from local map", { traceId });
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
- this.traceMap.set(traceId, {
195
- logger,
196
- spans: /* @__PURE__ */ new Map(),
197
- activeIds: /* @__PURE__ */ new Set(),
198
- isExternal
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
- * Creates a new logger per trace using config credentials
369
+ * Walk up the tree to find the MODEL_GENERATION ancestor span.
370
+ * Returns the BraintrustSpanData if found, undefined otherwise.
203
371
  */
204
- async initLoggerPerTrace(span) {
205
- if (this.traceMap.has(span.traceId)) {
206
- this.logger.debug("Braintrust exporter: Reusing existing trace from local map", { traceId: span.traceId });
207
- return;
208
- }
209
- try {
210
- const loggerInstance = await initLogger({
211
- projectName: this.config.projectName ?? "mastra-tracing",
212
- apiKey: this.config.apiKey,
213
- appUrl: this.config.endpoint,
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
- * Uses provided logger and detects external Braintrust spans.
224
- * If a Braintrust span is detected (from logger.traced() or Eval()), attaches to it.
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
- async initLoggerOrUseContext(span) {
228
- if (this.traceMap.has(span.traceId)) {
229
- this.logger.debug("Braintrust exporter: Reusing existing trace from local map", { traceId: span.traceId });
389
+ accumulateModelStepData(span, traceData) {
390
+ const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
391
+ if (!modelGenSpanData?.threadData) {
230
392
  return;
231
393
  }
232
- const braintrustSpan = currentSpan();
233
- if (braintrustSpan && braintrustSpan.id) {
234
- this.initTraceMap({ logger: braintrustSpan, isExternal: true, traceId: span.traceId });
235
- } else {
236
- this.initTraceMap({ logger: this.providedLogger, isExternal: false, traceId: span.traceId });
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
- getSpanData(options) {
240
- const { span, method } = options;
241
- if (this.traceMap.has(span.traceId)) {
242
- return this.traceMap.get(span.traceId);
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
- this.logger.warn("Braintrust exporter: No span data found for span", {
245
- traceId: span.traceId,
246
- spanId: span.id,
247
- spanName: span.name,
248
- spanType: span.type,
249
- isRootSpan: span.isRootSpan,
250
- parentSpanId: span.parentSpanId,
251
- method
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
- getBraintrustParent(options) {
255
- const { spanData, span, method } = options;
256
- const parentId = span.parentSpanId;
257
- if (!parentId) {
258
- return spanData.logger;
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.spans.has(parentId)) {
261
- return spanData.spans.get(parentId);
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
- if (parentId && !spanData.spans.has(parentId)) {
264
- return spanData.logger;
450
+ const hasToolCalls = threadData.some((step) => step.toolCalls && step.toolCalls.length > 0);
451
+ if (!hasToolCalls) {
452
+ return basePayload;
265
453
  }
266
- this.logger.warn("Braintrust exporter: No parent data found for span", {
267
- traceId: span.traceId,
268
- spanId: span.id,
269
- spanName: span.name,
270
- spanType: span.type,
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 (input && Array.isArray(input.messages)) {
282
- return input.messages;
283
- } else if (input && typeof input === "object" && "content" in input) {
284
- return [{ role: input.role, content: input.content }];
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
- spanType: span.type,
310
- ...span.metadata
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 };