From e9ba33aa93764af19d5d5b3e305e25f7a72d9b40 Mon Sep 17 00:00:00 2001 From: Trevor Colby <106617125+obs-gh-trevorColby@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:11:13 -0800 Subject: [PATCH 1/5] fix: check ai.operationId attribute for operation name transformation The transformOperationName function checks the span name to determine the gen_ai.operation.name attribute. However, by the time this function runs in onSpanEnd, the span name has already been transformed (e.g., from "ai.generateText" to "run.ai") by transformAiSdkSpanNames in onSpanStart. This means the check for 'generateText', 'streamText', etc. never matches, and the gen_ai.operation.name attribute is never set to 'chat'. This fix checks the ai.operationId attribute first, which is set by the Vercel AI SDK and contains the original operation ID (e.g., "ai.generateText"). This allows the function to correctly identify the operation type. Fixes #882 --- .../src/lib/tracing/ai-sdk-transformations.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 6f609533..7e2cc013 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -489,21 +489,42 @@ const transformVendor = (attributes: Record): void => { } }; +/** + * Transform span name to operation name for gen_ai.operation.name attribute. + * + * Note: The span name may have already been transformed by onSpanStart + * (e.g., "ai.generateText" -> "run.ai"). To handle this, we also check + * the ai.operationId attribute which contains the original operation ID. + * + * Fixes: https://github.com/traceloop/openllmetry-js/issues/882 + */ const transformOperationName = ( attributes: Record, spanName?: string, ): void => { - if (!spanName) return; + // Check ai.operationId attribute first (set by Vercel AI SDK) + // This is more reliable since span name may have been transformed already + const AI_OPERATION_ID = "ai.operationId"; + const operationId = attributes[AI_OPERATION_ID] as string | undefined; + + // Use operationId if available, otherwise fall back to spanName + const nameToCheck = operationId || spanName; + if (!nameToCheck) return; let operationName: string | undefined; if ( - spanName.includes("generateText") || - spanName.includes("streamText") || - spanName.includes("generateObject") || - spanName.includes("streamObject") + nameToCheck.includes("generateText") || + nameToCheck.includes("streamText") || + nameToCheck.includes("generateObject") || + nameToCheck.includes("streamObject") ) { operationName = "chat"; - } else if (spanName === "ai.toolCall" || spanName.endsWith(".tool")) { + } else if ( + nameToCheck === "ai.toolCall" || + nameToCheck.endsWith(".tool") || + spanName === "ai.toolCall" || + (spanName && spanName.endsWith(".tool")) + ) { operationName = "execute_tool"; } From 526fb88500a6a8249560d8d6ba8b7aba6dae2bcf Mon Sep 17 00:00:00 2001 From: Trevor Colby <106617125+obs-gh-trevorColby@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:27:33 -0800 Subject: [PATCH 2/5] fix: add type guard for operationId to prevent runtime errors The ai.operationId attribute may be non-string in some cases, which would cause nameToCheck.includes() to throw a runtime error. This fix adds a proper type guard to ensure operationId is a string before using it, falling back to spanName if it's not a string. --- .../traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 7e2cc013..28ac9562 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -505,7 +505,11 @@ const transformOperationName = ( // Check ai.operationId attribute first (set by Vercel AI SDK) // This is more reliable since span name may have been transformed already const AI_OPERATION_ID = "ai.operationId"; - const operationId = attributes[AI_OPERATION_ID] as string | undefined; + const operationIdValue = attributes[AI_OPERATION_ID]; + + // Ensure operationId is a string before using it (may be non-string in some cases) + const operationId = + typeof operationIdValue === "string" ? operationIdValue : undefined; // Use operationId if available, otherwise fall back to spanName const nameToCheck = operationId || spanName; From 330e622335e4ce727d4e45fe83e3a207e3cff858 Mon Sep 17 00:00:00 2001 From: "trevor.colby" Date: Wed, 4 Mar 2026 10:38:47 -0800 Subject: [PATCH 3/5] feat: add llm.request.type attribute for AI SDK spans --- .../src/lib/tracing/ai-sdk-transformations.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 28ac9562..caaa98e3 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -2,6 +2,7 @@ import { ReadableSpan, Span } from "@opentelemetry/sdk-trace-node"; import { SpanAttributes, TraceloopSpanKindValues, + LLMRequestTypeValues, } from "@traceloop/ai-semantic-conventions"; import { ATTR_GEN_AI_AGENT_NAME, @@ -498,6 +499,31 @@ const transformVendor = (attributes: Record): void => { * * Fixes: https://github.com/traceloop/openllmetry-js/issues/882 */ +const transformLlmRequestType = ( + attributes: Record, + nameToCheck?: string, +): void => { + if (!nameToCheck || attributes[SpanAttributes.LLM_REQUEST_TYPE]) { + return; + } + + let requestType: string | undefined; + if ( + nameToCheck.includes("generateText") || + nameToCheck.includes("streamText") || + nameToCheck.includes("generateObject") || + nameToCheck.includes("streamObject") + ) { + requestType = LLMRequestTypeValues.CHAT; + } + // Note: completion, rerank are not currently used by AI SDK + // embedding operations are handled separately by the SDK + + if (requestType) { + attributes[SpanAttributes.LLM_REQUEST_TYPE] = requestType; + } +}; + const transformOperationName = ( attributes: Record, spanName?: string, @@ -535,6 +561,9 @@ const transformOperationName = ( if (operationName) { attributes[ATTR_GEN_AI_OPERATION_NAME] = operationName; } + + // Also set llm.request.type for AI SDK spans + transformLlmRequestType(attributes, nameToCheck); }; const transformModelId = (attributes: Record): void => { From b0561840330b3fa567c3641ce79f87c983fde35e Mon Sep 17 00:00:00 2001 From: "trevor.colby" Date: Wed, 4 Mar 2026 13:24:23 -0800 Subject: [PATCH 4/5] refactor: move AI_OPERATION_ID constant to semantic conventions package - Add AI_OPERATION_ID to SpanAttributes in @traceloop/ai-semantic-conventions - Update ai-sdk-transformations.ts to import AI_OPERATION_ID from SpanAttributes - Remove local hardcoded AI_OPERATION_ID constant This centralizes the attribute key definition in the semantic conventions package for better maintainability and consistency across instrumentations. --- packages/ai-semantic-conventions/src/SemanticAttributes.ts | 3 +++ .../traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ai-semantic-conventions/src/SemanticAttributes.ts b/packages/ai-semantic-conventions/src/SemanticAttributes.ts index 8f4d9ef6..f0863f9d 100644 --- a/packages/ai-semantic-conventions/src/SemanticAttributes.ts +++ b/packages/ai-semantic-conventions/src/SemanticAttributes.ts @@ -30,6 +30,9 @@ export const SpanAttributes = { LLM_CHAT_STOP_SEQUENCES: "llm.chat.stop_sequences", LLM_REQUEST_FUNCTIONS: "llm.request.functions", + // AI SDK + AI_OPERATION_ID: "ai.operationId", + // Vector DB VECTOR_DB_VENDOR: "db.system", VECTOR_DB_QUERY_TOP_K: "db.vector.query.top_k", diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 42b54dd7..4c6cdd15 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -4,6 +4,8 @@ import { TraceloopSpanKindValues, LLMRequestTypeValues, } from "@traceloop/ai-semantic-conventions"; + +const { AI_OPERATION_ID } = SpanAttributes; import { ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_COMPLETION, @@ -531,7 +533,6 @@ const transformOperationName = ( ): void => { // Check ai.operationId attribute first (set by Vercel AI SDK) // This is more reliable since span name may have been transformed already - const AI_OPERATION_ID = "ai.operationId"; const operationIdValue = attributes[AI_OPERATION_ID]; // Ensure operationId is a string before using it (may be non-string in some cases) From 62464936d9d9a3f7cf6485ba04e1632f4a2649a6 Mon Sep 17 00:00:00 2001 From: "trevor.colby" Date: Wed, 4 Mar 2026 13:33:25 -0800 Subject: [PATCH 5/5] docs: fix JSDoc comment for transformLlmRequestType function The JSDoc incorrectly stated the function transforms gen_ai.operation.name, but it actually derives and sets the llm.request.type attribute. Updated the comment to accurately describe: - Function name: transformLlmRequestType - What it does: derives and sets llm.request.type attribute - How it works: examines span name or ai.operationId attribute - Why dual approach: handles prior onSpanStart transformations - References the linked issue #882 --- .../src/lib/tracing/ai-sdk-transformations.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 4c6cdd15..9dd3a9ae 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -494,11 +494,13 @@ const transformVendor = (attributes: Record): void => { }; /** - * Transform span name to operation name for gen_ai.operation.name attribute. + * Derives and sets the llm.request.type attribute for AI SDK operations. * - * Note: The span name may have already been transformed by onSpanStart - * (e.g., "ai.generateText" -> "run.ai"). To handle this, we also check - * the ai.operationId attribute which contains the original operation ID. + * The transformLlmRequestType function determines the request type (e.g., "chat") + * by examining either the span name or the ai.operationId attribute. This dual + * approach handles cases where the span name has already been transformed by + * onSpanStart (e.g., "ai.generateText" -> "run.ai"), ensuring the llm.request.type + * attribute is set correctly even after prior transformations. * * Fixes: https://github.com/traceloop/openllmetry-js/issues/882 */