@arizeai/openinference-genai 0.2.0 → 0.3.1

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/src/attributes.ts CHANGED
@@ -113,6 +113,14 @@ const TOOL_EXECUTION_PREFIXES = [
113
113
  ATTR_GEN_AI_TOOL_TYPE,
114
114
  ] as const;
115
115
 
116
+ const ATTR_GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" as const;
117
+ const ATTR_GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" as const;
118
+ const ATTR_GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" as const;
119
+ const ATTR_GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" as const;
120
+ const ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" as const;
121
+ const ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS =
122
+ "gen_ai.usage.cache_creation.input_tokens" as const;
123
+
116
124
  // Shared part parsing
117
125
  type AnyPart = GenAIInputMessagePart | GenAIOutputMessagePart;
118
126
 
@@ -129,6 +137,70 @@ const isGenAIChatMessage = (value: unknown): value is ChatMessage => {
129
137
  return true;
130
138
  };
131
139
 
140
+ /**
141
+ * Normalize a GenAI tool definition into the OpenAI-style tool schema shape expected by OpenInference.
142
+ * @param toolDefinition - The tool definition to normalize
143
+ * @returns The normalized tool definition, or the original value when it cannot be normalized
144
+ */
145
+ const normalizeToolDefinition = (toolDefinition: unknown): unknown => {
146
+ if (
147
+ typeof toolDefinition !== "object" ||
148
+ toolDefinition === null ||
149
+ Array.isArray(toolDefinition)
150
+ ) {
151
+ return toolDefinition;
152
+ }
153
+
154
+ const definition = toolDefinition as Record<string, unknown>;
155
+ if (typeof definition.function === "object" && definition.function !== null) {
156
+ return definition;
157
+ }
158
+
159
+ if (typeof definition.name !== "string") {
160
+ return definition;
161
+ }
162
+
163
+ const parameters = definition.parameters ?? definition.inputSchema ?? definition.input_schema;
164
+ const normalizedFunction: Record<string, unknown> = { name: definition.name };
165
+ if (typeof definition.description === "string") {
166
+ normalizedFunction.description = definition.description;
167
+ }
168
+ if (parameters != null) {
169
+ normalizedFunction.parameters = parameters;
170
+ }
171
+
172
+ return {
173
+ type: typeof definition.type === "string" ? definition.type : "function",
174
+ function: normalizedFunction,
175
+ };
176
+ };
177
+
178
+ const getSystemInstructionParts = (spanAttributes: Attributes): GenAIInputMessagePart[] | null => {
179
+ const systemInstructions = getString(spanAttributes[ATTR_GEN_AI_SYSTEM_INSTRUCTIONS]);
180
+ if (systemInstructions == null) {
181
+ return null;
182
+ }
183
+
184
+ const parsedInstructions = safelyParseJSON(systemInstructions);
185
+ if (Array.isArray(parsedInstructions)) {
186
+ const parts = parsedInstructions.flatMap((part): GenAIInputMessagePart[] => {
187
+ if (
188
+ typeof part === "object" &&
189
+ part !== null &&
190
+ "type" in part &&
191
+ part.type === "text" &&
192
+ "content" in part
193
+ ) {
194
+ return [{ type: "text", content: toStringContent(part.content) }];
195
+ }
196
+ return [];
197
+ });
198
+ return parts.length > 0 ? parts : null;
199
+ }
200
+
201
+ return [{ type: "text", content: systemInstructions }];
202
+ };
203
+
132
204
  /**
133
205
  * Process genai message parts into openinference attributes
134
206
  *
@@ -189,10 +261,7 @@ const processMessageParts = ({
189
261
  const response = toStringContent(part.response);
190
262
 
191
263
  set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`, id);
192
- const contentPrefix = `${msgPrefix}${SemanticConventions.MESSAGE_CONTENTS}.${contentIndex}.`;
193
- set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`, "text");
194
- set(attrs, `${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`, response);
195
- contentIndex += 1;
264
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_CONTENT}`, response);
196
265
  continue;
197
266
  }
198
267
  default: {
@@ -209,6 +278,30 @@ const processMessageParts = ({
209
278
  }
210
279
  };
211
280
 
281
+ /**
282
+ * Set OpenInference attributes for a single tool response message.
283
+ *
284
+ * GenAI can group multiple tool_call_response parts under one tool-role message;
285
+ * each response needs its own OpenInference message to avoid overwriting siblings.
286
+ * @param params - The tool response message mapping parameters
287
+ */
288
+ const setToolCallResponseMessage = ({
289
+ attrs,
290
+ msgPrefix,
291
+ part,
292
+ }: {
293
+ attrs: Attributes;
294
+ msgPrefix: string;
295
+ part: Extract<AnyPart, { type: "tool_call_response" }>;
296
+ }): void => {
297
+ const id = part.id ?? undefined;
298
+ const response = toStringContent(part.response);
299
+
300
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, "tool");
301
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`, id);
302
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_CONTENT}`, response);
303
+ };
304
+
212
305
  /**
213
306
  * Convert GenAI span attributes to OpenInference span attributes
214
307
  * @param spanAttributes - The span attributes containing GenAI span attributes to convert
@@ -225,6 +318,9 @@ export const convertGenAISpanAttributesToOpenInferenceSpanAttributes = (
225
318
  mapInvocationParameters(spanAttributes),
226
319
  mapInputMessages(spanAttributes),
227
320
  mapOutputMessages(spanAttributes),
321
+ mapAgent(spanAttributes),
322
+ mapSystemInstructions(spanAttributes),
323
+ mapToolDefinitions(spanAttributes),
228
324
  mapTokenCounts(spanAttributes),
229
325
  mapToolExecution(spanAttributes),
230
326
  mapInputValue(spanAttributes),
@@ -241,6 +337,7 @@ export const convertGenAISpanAttributesToOpenInferenceSpanAttributes = (
241
337
  export const mapProviderAndSystem = (spanAttributes: Attributes): Attributes => {
242
338
  const attrs: Attributes = {};
243
339
  const provider = getString(spanAttributes[ATTR_GEN_AI_PROVIDER_NAME]);
340
+ set(attrs, SemanticConventions.LLM_SYSTEM, provider);
244
341
  set(attrs, SemanticConventions.LLM_PROVIDER, provider);
245
342
  return attrs;
246
343
  };
@@ -366,6 +463,9 @@ export const mapInputValue = (spanAttributes: Attributes): Attributes => {
366
463
  // fallback to deprecated prompt attribute if input is not present
367
464
  input = getString(spanAttributes[ATTR_GEN_AI_PROMPT]);
368
465
  }
466
+ if (!input) {
467
+ input = getString(spanAttributes[ATTR_GEN_AI_INPUT_MESSAGES]);
468
+ }
369
469
  // only set input value and mime type if input is present
370
470
  if (input) {
371
471
  set(attrs, SemanticConventions.INPUT_VALUE, input);
@@ -386,6 +486,9 @@ export const mapOutputValue = (spanAttributes: Attributes): Attributes => {
386
486
  // fallback to deprecated completion attribute if output is not present
387
487
  output = getString(spanAttributes[ATTR_GEN_AI_COMPLETION]);
388
488
  }
489
+ if (!output) {
490
+ output = getString(spanAttributes[ATTR_GEN_AI_OUTPUT_MESSAGES]);
491
+ }
389
492
  // only set output value and mime type if output is present
390
493
  if (output) {
391
494
  set(attrs, SemanticConventions.OUTPUT_VALUE, output);
@@ -394,6 +497,60 @@ export const mapOutputValue = (spanAttributes: Attributes): Attributes => {
394
497
  return attrs;
395
498
  };
396
499
 
500
+ /**
501
+ * Map GenAI agent attributes to OpenInference attributes.
502
+ * @param spanAttributes - The span attributes containing agent details
503
+ * @returns The mapped agent attributes
504
+ */
505
+ export const mapAgent = (spanAttributes: Attributes): Attributes => {
506
+ const attrs: Attributes = {};
507
+ set(attrs, SemanticConventions.AGENT_NAME, getString(spanAttributes[ATTR_GEN_AI_AGENT_NAME]));
508
+ return attrs;
509
+ };
510
+
511
+ /**
512
+ * Map GenAI system instructions into metadata for preservation.
513
+ * @param spanAttributes - The span attributes containing system instructions
514
+ * @returns The mapped metadata attributes
515
+ */
516
+ export const mapSystemInstructions = (spanAttributes: Attributes): Attributes => {
517
+ const attrs: Attributes = {};
518
+ const systemInstructions = getString(spanAttributes[ATTR_GEN_AI_SYSTEM_INSTRUCTIONS]);
519
+ if (systemInstructions) {
520
+ set(attrs, `${SemanticConventions.METADATA}.gen_ai.system_instructions`, systemInstructions);
521
+
522
+ const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.0.`;
523
+ set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, "system");
524
+ processMessageParts({
525
+ attrs,
526
+ msgPrefix,
527
+ parts: getSystemInstructionParts(spanAttributes) ?? [],
528
+ });
529
+ }
530
+ return attrs;
531
+ };
532
+
533
+ /**
534
+ * Map GenAI tool definitions to OpenInference LLM tool schema attributes.
535
+ * @param spanAttributes - The span attributes containing tool definitions
536
+ * @returns The mapped tool schema attributes
537
+ */
538
+ export const mapToolDefinitions = (spanAttributes: Attributes): Attributes => {
539
+ const attrs: Attributes = {};
540
+ const toolDefinitions = getString(spanAttributes[ATTR_GEN_AI_TOOL_DEFINITIONS]);
541
+ const parsedToolDefinitions = safelyParseJSON(toolDefinitions);
542
+ if (Array.isArray(parsedToolDefinitions)) {
543
+ parsedToolDefinitions.forEach((toolDefinition, index) => {
544
+ set(
545
+ attrs,
546
+ `${SemanticConventions.LLM_TOOLS}.${index}.${SemanticConventions.TOOL_JSON_SCHEMA}`,
547
+ safelyJSONStringify(normalizeToolDefinition(toolDefinition)),
548
+ );
549
+ });
550
+ }
551
+ return attrs;
552
+ };
553
+
397
554
  /**
398
555
  * Map input messages to openinference attributes
399
556
  * @param spanAttributes - The span attributes containing input messages to map
@@ -404,13 +561,30 @@ export const mapInputMessages = (spanAttributes: Attributes): Attributes => {
404
561
  const genAIInputMessages = safelyParseJSON(spanAttributes[ATTR_GEN_AI_INPUT_MESSAGES]);
405
562
 
406
563
  if (Array.isArray(genAIInputMessages)) {
407
- (genAIInputMessages as unknown[]).forEach((msg, msgIndex) => {
564
+ let msgIndex = getSystemInstructionParts(spanAttributes) != null ? 1 : 0;
565
+ (genAIInputMessages as unknown[]).forEach((msg) => {
408
566
  if (!isGenAIChatMessage(msg)) return;
567
+
568
+ const toolCallResponses = msg.parts.filter(
569
+ (part): part is Extract<AnyPart, { type: "tool_call_response" }> =>
570
+ part?.type === "tool_call_response",
571
+ );
572
+
573
+ if (msg.role === "tool" && toolCallResponses.length > 0) {
574
+ toolCallResponses.forEach((part) => {
575
+ const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${msgIndex}.`;
576
+ setToolCallResponseMessage({ attrs, msgPrefix, part });
577
+ msgIndex += 1;
578
+ });
579
+ return;
580
+ }
581
+
409
582
  const msgPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${msgIndex}.`;
410
583
  // set the message role
411
584
  set(attrs, `${msgPrefix}${SemanticConventions.MESSAGE_ROLE}`, msg.role);
412
585
  // process and set the rest of the message parts
413
586
  processMessageParts({ attrs, msgPrefix, parts: msg.parts });
587
+ msgIndex += 1;
414
588
  });
415
589
  }
416
590
 
@@ -450,6 +624,10 @@ export const mapTokenCounts = (spanAttributes: Attributes): Attributes => {
450
624
  const attrs: Attributes = {};
451
625
  const inputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS]);
452
626
  const outputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]);
627
+ const cacheReadInputTokens = getNumber(spanAttributes[ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]);
628
+ const cacheCreationInputTokens = getNumber(
629
+ spanAttributes[ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS],
630
+ );
453
631
  if (typeof inputTokens === "number") {
454
632
  set(attrs, SemanticConventions.LLM_TOKEN_COUNT_PROMPT, inputTokens);
455
633
  }
@@ -459,6 +637,16 @@ export const mapTokenCounts = (spanAttributes: Attributes): Attributes => {
459
637
  if (typeof inputTokens === "number" && typeof outputTokens === "number") {
460
638
  set(attrs, SemanticConventions.LLM_TOKEN_COUNT_TOTAL, inputTokens + outputTokens);
461
639
  }
640
+ if (typeof cacheReadInputTokens === "number") {
641
+ set(attrs, SemanticConventions.LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ, cacheReadInputTokens);
642
+ }
643
+ if (typeof cacheCreationInputTokens === "number") {
644
+ set(
645
+ attrs,
646
+ SemanticConventions.LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_WRITE,
647
+ cacheCreationInputTokens,
648
+ );
649
+ }
462
650
  return attrs;
463
651
  };
464
652
 
@@ -473,11 +661,21 @@ export const mapToolExecution = (spanAttributes: Attributes): Attributes => {
473
661
  const toolName = getString(spanAttributes[ATTR_GEN_AI_TOOL_NAME]);
474
662
  const toolDescription = getString(spanAttributes[ATTR_GEN_AI_TOOL_DESCRIPTION]);
475
663
  const toolCallId = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_ID]);
664
+ const toolCallArguments = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_ARGUMENTS]);
665
+ const toolCallResult = getString(spanAttributes[ATTR_GEN_AI_TOOL_CALL_RESULT]);
476
666
  // parse supported tool details
477
- // note: while openinference can track parameters, gen_ai does not provide this information
478
667
  set(attrs, SemanticConventions.TOOL_NAME, toolName);
479
668
  set(attrs, SemanticConventions.TOOL_DESCRIPTION, toolDescription);
480
669
  set(attrs, SemanticConventions.TOOL_CALL_ID, toolCallId);
670
+ set(attrs, SemanticConventions.TOOL_PARAMETERS, toolCallArguments);
671
+ if (toolCallArguments) {
672
+ set(attrs, SemanticConventions.INPUT_VALUE, toolCallArguments);
673
+ set(attrs, SemanticConventions.INPUT_MIME_TYPE, getMimeType(toolCallArguments));
674
+ }
675
+ if (toolCallResult) {
676
+ set(attrs, SemanticConventions.OUTPUT_VALUE, toolCallResult);
677
+ set(attrs, SemanticConventions.OUTPUT_MIME_TYPE, getMimeType(toolCallResult));
678
+ }
481
679
 
482
680
  return attrs;
483
681
  };