@@ -129,7 +129,7 @@ export function toDate(value: Date | string | number): Date {
129129 */
130130export function safeStringify ( data : unknown ) : string {
131131 try {
132- return JSON . stringify ( data ) ;
132+ return JSON . stringify ( data ) ?? '' ;
133133 } catch {
134134 if ( typeof data === 'object' && data !== null ) {
135135 return `[Non-serializable ${ data . constructor ?. name || 'Object' } ]` ;
@@ -145,22 +145,34 @@ function isMessageArray(data: any): data is Array<{ role: string; content: any }
145145 return Array . isArray ( data ) && data . every ( m => m ?. role && m ?. content !== undefined ) ;
146146}
147147
148+ function isModelDataSpan ( spanType : SpanType ) : boolean {
149+ return (
150+ spanType === SpanType . MODEL_GENERATION || spanType === SpanType . MODEL_STEP || spanType === SpanType . MODEL_INFERENCE
151+ ) ;
152+ }
153+
148154/**
149155 * Checks if data is in Gemini content array format ({role, parts}[]).
150156 */
151157function isGeminiContentArray ( data : any ) : data is Array < { role : string ; parts : any [ ] } > {
152158 return Array . isArray ( data ) && data . every ( m => m ?. role && Array . isArray ( m ?. parts ) ) ;
153159}
154160
161+ function toMessageContent ( content : any ) : string {
162+ return typeof content === 'string' ? content : safeStringify ( content ) ;
163+ }
164+
155165/**
156166 * Maps a {role, content}[] message array into the Datadog message shape,
157167 * stringifying any non-string content (e.g. multimodal part arrays).
158168 */
159169function toDatadogMessages ( messages : Array < { role : string ; content : any } > ) : Array < { role : string ; content : string } > {
160- return messages . map ( m => ( {
161- role : m . role ,
162- content : typeof m . content === 'string' ? m . content : safeStringify ( m . content ) ,
163- } ) ) ;
170+ return messages
171+ . map ( m => ( {
172+ role : m . role ,
173+ content : toMessageContent ( m . content ) ,
174+ } ) )
175+ . filter ( m => ! ( m . role === 'user' && m . content . trim ( ) . length === 0 ) ) ;
164176}
165177
166178/**
@@ -182,18 +194,18 @@ function geminiContentToMessage(item: { role: string; parts: any[] }): { role: s
182194
183195/**
184196 * Formats input data for Datadog annotations.
185- * LLM spans use message array format; others use raw or stringified data.
197+ * Model spans use message array format; others use raw or stringified data.
186198 */
187199export function formatInput ( input : any , spanType : SpanType ) : any {
188- // LLM spans expect {role, content}[] format
189- if ( spanType === SpanType . MODEL_GENERATION || spanType === SpanType . MODEL_STEP ) {
200+ // Model spans expect {role, content}[] format
201+ if ( isModelDataSpan ( spanType ) ) {
190202 // Already in message format
191203 if ( isMessageArray ( input ) ) {
192204 return toDatadogMessages ( input ) ;
193205 }
194206 // Gemini format: {role, parts} → normalize to {role, content}
195207 if ( isGeminiContentArray ( input ) ) {
196- return input . map ( geminiContentToMessage ) ;
208+ return toDatadogMessages ( input . map ( geminiContentToMessage ) ) ;
197209 }
198210 // Mastra wraps MODEL_GENERATION input as { messages, schema? } and Gemini
199211 // request bodies use { contents }. Unwrap so we don't bury the message array
@@ -203,32 +215,32 @@ export function formatInput(input: any, spanType: SpanType): any {
203215 return toDatadogMessages ( ( input as any ) . messages ) ;
204216 }
205217 if ( isGeminiContentArray ( ( input as any ) . messages ) ) {
206- return ( input as any ) . messages . map ( geminiContentToMessage ) ;
218+ return toDatadogMessages ( ( input as any ) . messages . map ( geminiContentToMessage ) ) ;
207219 }
208220 if ( isGeminiContentArray ( ( input as any ) . contents ) ) {
209- return ( input as any ) . contents . map ( geminiContentToMessage ) ;
221+ return toDatadogMessages ( ( input as any ) . contents . map ( geminiContentToMessage ) ) ;
210222 }
211223 }
212224 // String input becomes user message
213225 if ( typeof input === 'string' ) {
214- return [ { role : 'user' , content : input } ] ;
226+ return toDatadogMessages ( [ { role : 'user' , content : input } ] ) ;
215227 }
216228 // Object input gets stringified as user message
217- return [ { role : 'user' , content : safeStringify ( input ) } ] ;
229+ return toDatadogMessages ( [ { role : 'user' , content : safeStringify ( input ) } ] ) ;
218230 }
219231
220- // Non-LLM spans: pass through strings/arrays, stringify objects
232+ // Non-model spans: pass through strings/arrays, stringify objects
221233 if ( typeof input === 'string' || Array . isArray ( input ) ) return input ;
222234 return safeStringify ( input ) ;
223235}
224236
225237/**
226238 * Formats output data for Datadog annotations.
227- * LLM spans use message array format; others use raw or stringified data.
239+ * Model spans use message array format; others use raw or stringified data.
228240 */
229241export function formatOutput ( output : any , spanType : SpanType ) : any {
230- // LLM spans expect {role, content}[] format
231- if ( spanType === SpanType . MODEL_GENERATION || spanType === SpanType . MODEL_STEP ) {
242+ // Model spans expect {role, content}[] format
243+ if ( isModelDataSpan ( spanType ) ) {
232244 // Already in message format
233245 if ( isMessageArray ( output ) ) {
234246 return toDatadogMessages ( output ) ;
@@ -256,7 +268,7 @@ export function formatOutput(output: any, spanType: SpanType): any {
256268 return [ { role : 'assistant' , content : safeStringify ( output ) } ] ;
257269 }
258270
259- // Non-LLM spans: pass through strings, stringify objects
271+ // Non-model spans: pass through strings, stringify objects
260272 if ( typeof output === 'string' ) return output ;
261273 return safeStringify ( output ) ;
262274}
0 commit comments