11import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes' ;
22import type { SpanAttributeValue } from '../../types/span' ;
3+ import { stringify } from '../../utils/string' ;
34import {
45 GEN_AI_AGENT_NAME_ATTRIBUTE ,
56 GEN_AI_INPUT_MESSAGES_ATTRIBUTE ,
@@ -27,7 +28,7 @@ import {
2728 GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ,
2829} from '../ai/gen-ai-attributes' ;
2930import { isContentMedia , stripInlineMediaFromSingleMessage } from '../ai/mediaStripping' ;
30- import { extractSystemInstructions , getJsonString , getTruncatedJsonString } from '../ai/utils' ;
31+ import { extractSystemInstructions , getTruncatedJsonString } from '../ai/utils' ;
3132import { LANGCHAIN_ORIGIN , ROLE_MAP } from './constants' ;
3233import type { LangChainLLMResult , LangChainMessage , LangChainSerialized } from './types' ;
3334
@@ -50,19 +51,6 @@ const setNumberIfDefined = (target: Record<string, SpanAttributeValue>, key: str
5051 if ( ! Number . isNaN ( n ) ) target [ key ] = n ;
5152} ;
5253
53- /**
54- * Converts a value to a string. Avoids double-quoted JSON strings where a plain
55- * string is desired, but still handles objects/arrays safely.
56- */
57- function asString ( v : unknown ) : string {
58- if ( typeof v === 'string' ) return v ;
59- try {
60- return JSON . stringify ( v ) ;
61- } catch {
62- return String ( v ) ;
63- }
64- }
65-
6654/**
6755 * Converts message content to a string, stripping inline media (base64 images, audio, etc.)
6856 * from multimodal content before stringification so downstream media stripping can't miss it.
@@ -78,7 +66,7 @@ function asString(v: unknown): string {
7866 * ])
7967 * // => '[{"type":"text","text":"What color?"},{"type":"image_url","image_url":{"url":"[Blob substitute]"}}]'
8068 *
81- * // Without this, asString() would JSON.stringify the raw array and the base64 blob
69+ * // Without this, stringification would JSON.stringify the raw array and the base64 blob
8270 * // would end up in span attributes, since downstream stripping only works on objects.
8371 */
8472function normalizeContent ( v : unknown ) : string {
@@ -92,7 +80,7 @@ function normalizeContent(v: unknown): string {
9280 return String ( v ) ;
9381 }
9482 }
95- return asString ( v ) ;
83+ return stringify ( v , String ) ;
9684}
9785
9886/**
@@ -264,9 +252,9 @@ function baseRequestAttributes(
264252 langSmithMetadata ?: Record < string , unknown > ,
265253) : Record < string , SpanAttributeValue > {
266254 return {
267- [ GEN_AI_SYSTEM_ATTRIBUTE ] : asString ( system ?? 'langchain' ) ,
255+ [ GEN_AI_SYSTEM_ATTRIBUTE ] : stringify ( system ?? 'langchain' , String ) ,
268256 [ GEN_AI_OPERATION_NAME_ATTRIBUTE ] : 'chat' ,
269- [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] : asString ( modelName ) ,
257+ [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] : stringify ( modelName , String ) ,
270258 [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : LANGCHAIN_ORIGIN ,
271259 ...extractCommonRequestAttributes ( serialized , invocationParams , langSmithMetadata ) ,
272260 } ;
@@ -299,7 +287,7 @@ export function extractLLMRequestAttributes(
299287 setIfDefined (
300288 attrs ,
301289 GEN_AI_INPUT_MESSAGES_ATTRIBUTE ,
302- enableTruncation ? getTruncatedJsonString ( messages ) : getJsonString ( messages ) ,
290+ enableTruncation ? getTruncatedJsonString ( messages ) : stringify ( messages ) ,
303291 ) ;
304292 }
305293
@@ -343,7 +331,7 @@ export function extractChatModelRequestAttributes(
343331 setIfDefined (
344332 attrs ,
345333 GEN_AI_INPUT_MESSAGES_ATTRIBUTE ,
346- enableTruncation ? getTruncatedJsonString ( filteredMessages ) : getJsonString ( filteredMessages ) ,
334+ enableTruncation ? getTruncatedJsonString ( filteredMessages ) : stringify ( filteredMessages ) ,
347335 ) ;
348336 }
349337
@@ -378,7 +366,7 @@ function addToolCallsAttributes(generations: LangChainMessage[][], attrs: Record
378366 }
379367
380368 if ( toolCalls . length > 0 ) {
381- setIfDefined ( attrs , GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE , asString ( toolCalls ) ) ;
369+ setIfDefined ( attrs , GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE , stringify ( toolCalls , String ) ) ;
382370 }
383371}
384372
@@ -466,7 +454,7 @@ export function extractLlmResponseAttributes(
466454 . filter ( ( r ) : r is string => typeof r === 'string' ) ;
467455
468456 if ( finishReasons . length > 0 ) {
469- setIfDefined ( attrs , GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE , asString ( finishReasons ) ) ;
457+ setIfDefined ( attrs , GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE , stringify ( finishReasons , String ) ) ;
470458 }
471459
472460 // Tool calls metadata (names, IDs) are not PII, so capture them regardless of recordOutputs
@@ -479,7 +467,7 @@ export function extractLlmResponseAttributes(
479467 . filter ( t => typeof t === 'string' ) ;
480468
481469 if ( texts . length > 0 ) {
482- setIfDefined ( attrs , GEN_AI_RESPONSE_TEXT_ATTRIBUTE , asString ( texts ) ) ;
470+ setIfDefined ( attrs , GEN_AI_RESPONSE_TEXT_ATTRIBUTE , stringify ( texts , String ) ) ;
483471 }
484472 }
485473 }
@@ -506,7 +494,7 @@ export function extractLlmResponseAttributes(
506494 // Stop reason: v1 stores this in message.response_metadata.finish_reason
507495 const stopReason = llmOutput ?. stop_reason ?? v1Message ?. response_metadata ?. finish_reason ;
508496 if ( stopReason ) {
509- setIfDefined ( attrs , GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE , asString ( stopReason ) ) ;
497+ setIfDefined ( attrs , GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE , stringify ( stopReason , String ) ) ;
510498 }
511499
512500 return attrs ;
0 commit comments