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