import type { Attributes } from "@opentelemetry/api"; import type { Response as ResponseType, ResponseCreateParamsBase, ResponseInputItem, ResponseOutputItem, ResponseStreamEvent, } from "openai/resources/responses/responses"; import type { Stream } from "openai/streaming"; import { safelyJSONStringify } from "@arizeai/openinference-core"; import { SemanticConventions } from "@arizeai/openinference-semantic-conventions"; /** * Get attributes for responses api Items that are not typical messages with role * @param item - The item to get attributes for * @param prefix - The prefix to use for the attributes * @returns The attributes for the item */ function getResponseItemAttributes( item: Exclude, prefix = "", ): Attributes { const attributes: Attributes = {}; // all items that are not typical messages with role // things like images, files, etc. const toolCallPrefix = `${prefix}${SemanticConventions.MESSAGE_TOOL_CALLS}.0.`; switch (item.type) { case "function_call": { attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant"; // now using tool call prefix to simulate multiple tool calls per message attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] = item.call_id; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`] = item.name; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON}`] = item.arguments; break; } case "function_call_output": { attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool"; attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] = item.call_id; if (typeof item.output === "string") { attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] = item.output; } else { // TODO(2410): figure out how to serialize the list of tools attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] = safelyJSONStringify(item.output) || undefined; } break; } case "reasoning": { attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant"; const contentPrefix = `${prefix}${SemanticConventions.MESSAGE_CONTENTS}.0.`; attributes[`${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`] = "reasoning"; const texts = item.summary.filter((s) => s.type === "summary_text").map((s) => s.text); if (texts.length > 0) { attributes[`${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`] = texts.join("\n"); } if (item.encrypted_content && item.encrypted_content.length > 0) { attributes[`${contentPrefix}${SemanticConventions.MESSAGE_CONTENT_ENCRYPTED_CONTENT}`] = item.encrypted_content; } break; } case "item_reference": { break; } case "file_search_call": { if (!item.results) { // its a tool call attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant"; // now using tool call prefix to simulate multiple tool calls per message attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] = item.id; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`] = item.type; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON}`] = JSON.stringify(item.queries); } else { // its a tool call output attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool"; attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] = item.id; attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] = JSON.stringify( item.results, ); } break; } case "computer_call": { attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant"; // now using tool call prefix to simulate multiple tool calls per message attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] = item.id; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`] = item.type; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON}`] = JSON.stringify(item.action); break; } case "computer_call_output": { attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool"; attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] = item.call_id; attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] = JSON.stringify(item.output); break; } case "web_search_call": { attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant"; // now using tool call prefix to simulate multiple tool calls per message attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] = item.id; attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`] = item.type; // web search call does not share its arguments with the caller // it will show "undefined" in the arguments when traced break; } } return attributes; } /** * Get attributes for responses api Item, input or output * Non message items are detected and handled by {@link getResponseItemAttributes} * @param itemMessage - The message item to get attributes for * @param prefix - The prefix to use for the attributes * @returns The attributes for the message item */ function getResponseItemMessageAttributes( itemMessage: ResponseInputItem | ResponseOutputItem, prefix = "", ): Attributes { const message = typeof itemMessage === "string" ? ({ content: itemMessage, role: "user" } satisfies ResponseInputItem) : itemMessage; if (!("role" in message)) { return getResponseItemAttributes(message, prefix); } const role = message.role; const attributes: Attributes = { [`${prefix}${SemanticConventions.MESSAGE_ROLE}`]: role, }; // add contents from message if (typeof message.content === "string") { attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] = message.content; } else if (Array.isArray(message.content)) { message.content.forEach((part, index) => { const contentsIndexPrefix = `${prefix}${SemanticConventions.MESSAGE_CONTENTS}.${index}.`; if (part.type === "input_text") { attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`] = "input_text"; attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`] = part.text; } else if (part.type === "output_text") { attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`] = "output_text"; attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`] = part.text; } else if (part.type === "input_image") { attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`] = "input_image"; if (part.image_url) { attributes[ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_IMAGE}.${SemanticConventions.IMAGE_URL}` ] = part.image_url; } } else if (part.type === "input_file") { // TODO: Handle input file } else if (part.type === "refusal") { attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`] = "refusal"; attributes[`${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`] = part.refusal; } }); } // add role based fields to attributes // as of now, there are no role based fields as they are handled by alternatively typed // items switch (message.role) { case "user": { break; } case "assistant": { break; } case "system": { break; } case "developer": { break; } } return attributes; } export function getResponsesInputMessagesAttributes(body: ResponseCreateParamsBase): Attributes { const attributes: Attributes = {}; const items: ResponseInputItem[] = []; if (body.instructions) { items.push({ content: body.instructions, role: "system" }); } if (typeof body.input === "string") { items.push({ content: body.input, role: "user" }); } else { items.push(...(body.input ?? [])); } items.forEach((item, index) => { const indexPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}.`; const itemAttributes = getResponseItemMessageAttributes(item, indexPrefix); Object.entries(itemAttributes).forEach(([key, value]) => { attributes[key] = value; }); }); return attributes; } export function getResponsesUsageAttributes(response: ResponseType): Attributes { if (response.usage) { return { [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]: response.usage.output_tokens, [SemanticConventions.LLM_TOKEN_COUNT_PROMPT]: response.usage.input_tokens, [SemanticConventions.LLM_TOKEN_COUNT_TOTAL]: response.usage.total_tokens, [SemanticConventions.LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ]: response.usage.input_tokens_details?.cached_tokens, [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING]: response.usage.output_tokens_details?.reasoning_tokens, // no audio tokens for response inputs or outputs }; } return {}; } export function getResponsesOutputMessagesAttributes(response: ResponseType): Attributes { const attributes: Attributes = {}; const items = response.output; items.forEach((item, index) => { const indexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}.`; const itemAttributes = getResponseItemMessageAttributes(item, indexPrefix); Object.entries(itemAttributes).forEach(([key, value]) => { attributes[key] = value; }); }); return attributes; } export async function consumeResponseStreamEvents( stream: Stream, ): Promise { let response: ResponseType | undefined; for await (const event of stream) { switch (event.type) { case "response.completed": { response = event.response; break; } } } return response; }