From 707e5677bd7ce7d1a9c9356625c572651ffff96c Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:54:56 +0200 Subject: [PATCH 01/24] added transform --- .../src/SemanticAttributes.ts | 9 + .../src/lib/tracing/ai-sdk-transformations.ts | 117 +++- .../test/ai-sdk-transformations.test.ts | 525 ++++++++++++++++++ 3 files changed, 650 insertions(+), 1 deletion(-) diff --git a/packages/ai-semantic-conventions/src/SemanticAttributes.ts b/packages/ai-semantic-conventions/src/SemanticAttributes.ts index 9cea5d3a..4d0b049b 100644 --- a/packages/ai-semantic-conventions/src/SemanticAttributes.ts +++ b/packages/ai-semantic-conventions/src/SemanticAttributes.ts @@ -39,6 +39,15 @@ export const SpanAttributes = { "gen_ai.usage.cache_creation_input_tokens", GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: "gen_ai.usage.cache_read_input_tokens", GEN_AI_USAGE_REASONING_TOKENS: "gen_ai.usage.reasoning_tokens", + GEN_AI_OPERATION_NAME: "gen_ai.operation.name", + GEN_AI_PROVIDER_NAME: "gen_ai.provider.name", + GEN_AI_TOOL_NAME: "gen_ai.tool.name", + GEN_AI_TOOL_CALL_ID: "gen_ai.tool.call.id", + GEN_AI_TOOL_CALL_ARGUMENTS: "gen_ai.tool.call.arguments", + GEN_AI_TOOL_CALL_RESULT: "gen_ai.tool.call.result", + GEN_AI_RESPONSE_ID: "gen_ai.response.id", + GEN_AI_RESPONSE_FINISH_REASONS: "gen_ai.response.finish_reasons", + GEN_AI_CONVERSATION_ID: "gen_ai.conversation.id", GEN_AI_AGENT_NAME: "gen_ai.agent.name", 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 fdf378c4..b9597fcc 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -432,6 +432,10 @@ const transformVendor = (attributes: Record): void => { // Find matching vendor prefix in mapping let mappedVendor = null; if (typeof vendor === "string" && vendor.length > 0) { + // Extract base provider name for OpenTelemetry standard (e.g., "openai" from "openai.chat") + const providerName = vendor.split(".")[0]; + attributes[SpanAttributes.GEN_AI_PROVIDER_NAME] = providerName; + for (const prefix of Object.keys(VENDOR_MAPPING)) { if (vendor.startsWith(prefix)) { mappedVendor = VENDOR_MAPPING[prefix]; @@ -445,6 +449,111 @@ const transformVendor = (attributes: Record): void => { } }; +const transformOperationName = ( + attributes: Record, + spanName?: string, +): void => { + if (!spanName) return; + + let operationName: string | undefined; + if ( + spanName.includes("generateText") || + spanName.includes("streamText") || + spanName.includes("generateObject") || + spanName.includes("streamObject") + ) { + operationName = "chat"; + } else if (spanName === "ai.toolCall" || spanName.endsWith(".tool")) { + operationName = "execute_tool"; + } + + if (operationName) { + attributes[SpanAttributes.GEN_AI_OPERATION_NAME] = operationName; + } +}; + +const transformModelId = (attributes: Record): void => { + const AI_MODEL_ID = "ai.model.id"; + if (AI_MODEL_ID in attributes) { + attributes[SpanAttributes.LLM_REQUEST_MODEL] = attributes[AI_MODEL_ID]; + delete attributes[AI_MODEL_ID]; + } +}; + +const transformFinishReason = (attributes: Record): void => { + const AI_RESPONSE_FINISH_REASON = "ai.response.finishReason"; + if (AI_RESPONSE_FINISH_REASON in attributes) { + const finishReason = attributes[AI_RESPONSE_FINISH_REASON]; + // Convert to array format for OTel standard + attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS] = Array.isArray( + finishReason, + ) + ? finishReason + : [finishReason]; + delete attributes[AI_RESPONSE_FINISH_REASON]; + } +}; + +const transformToolCallAttributes = ( + attributes: Record, +): void => { + // Transform tool name + if ("ai.toolCall.name" in attributes) { + attributes[SpanAttributes.GEN_AI_TOOL_NAME] = + attributes["ai.toolCall.name"]; + // Keep ai.toolCall.name for now, will be deleted in transformToolCalls + } + + // Transform tool call ID + if ("ai.toolCall.id" in attributes) { + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID] = + attributes["ai.toolCall.id"]; + delete attributes["ai.toolCall.id"]; + } + + // Transform tool arguments (keep both OTel and Traceloop versions) + if ("ai.toolCall.args" in attributes) { + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS] = + attributes["ai.toolCall.args"]; + // Don't delete yet - transformToolCalls will handle entity input/output + } + + // Transform tool result (keep both OTel and Traceloop versions) + if ("ai.toolCall.result" in attributes) { + attributes[SpanAttributes.GEN_AI_TOOL_CALL_RESULT] = + attributes["ai.toolCall.result"]; + // Don't delete yet - transformToolCalls will handle entity input/output + } +}; + +const transformConversationId = (attributes: Record): void => { + // Check for conversation/session ID in metadata + const conversationId = attributes["ai.telemetry.metadata.conversationId"]; + const sessionId = attributes["ai.telemetry.metadata.sessionId"]; + + if (conversationId) { + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID] = conversationId; + } else if (sessionId) { + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID] = sessionId; + } +}; + +const transformResponseMetadata = (attributes: Record): void => { + const AI_RESPONSE_MODEL = "ai.response.model"; + const AI_RESPONSE_ID = "ai.response.id"; + + if (AI_RESPONSE_MODEL in attributes) { + attributes[SpanAttributes.LLM_RESPONSE_MODEL] = + attributes[AI_RESPONSE_MODEL]; + delete attributes[AI_RESPONSE_MODEL]; + } + + if (AI_RESPONSE_ID in attributes) { + attributes[SpanAttributes.GEN_AI_RESPONSE_ID] = attributes[AI_RESPONSE_ID]; + delete attributes[AI_RESPONSE_ID]; + } +}; + const transformTelemetryMetadata = ( attributes: Record, spanName?: string, @@ -525,6 +634,8 @@ export const transformLLMSpans = ( attributes: Record, spanName?: string, ): void => { + transformOperationName(attributes, spanName); + transformModelId(attributes); transformResponseText(attributes); transformResponseObject(attributes); transformResponseToolCalls(attributes); @@ -533,8 +644,12 @@ export const transformLLMSpans = ( transformPromptTokens(attributes); transformCompletionTokens(attributes); transformProviderMetadata(attributes); + transformFinishReason(attributes); + transformResponseMetadata(attributes); calculateTotalTokens(attributes); - transformVendor(attributes); + transformVendor(attributes); // Also sets GEN_AI_PROVIDER_NAME + transformConversationId(attributes); + transformToolCallAttributes(attributes); transformTelemetryMetadata(attributes, spanName); }; diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index af50a7c0..76090492 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -1895,4 +1895,529 @@ describe("AI SDK Transformations", () => { assert.strictEqual(mockSpan.attributes["ai.toolCall.result"], undefined); }); }); + + describe("transformLLMSpans - operation name", () => { + it("should transform generateText span to chat operation", () => { + const attributes = { + "ai.response.text": "Hello!", + }; + + transformLLMSpans(attributes, "ai.generateText"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "chat", + ); + }); + + it("should transform streamText span to chat operation", () => { + const attributes = { + "ai.response.text": "Hello!", + }; + + transformLLMSpans(attributes, "ai.streamText"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "chat", + ); + }); + + it("should transform generateObject span to chat operation", () => { + const attributes = { + "ai.response.object": '{"result":"test"}', + }; + + transformLLMSpans(attributes, "ai.generateObject"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "chat", + ); + }); + + it("should transform streamObject span to chat operation", () => { + const attributes = { + "ai.response.object": '{"result":"test"}', + }; + + transformLLMSpans(attributes, "ai.streamObject"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "chat", + ); + }); + + it("should transform toolCall span to execute_tool operation", () => { + const attributes = { + "ai.toolCall.name": "getWeather", + }; + + transformLLMSpans(attributes, "ai.toolCall"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "execute_tool", + ); + }); + + it("should transform .tool span name to execute_tool operation", () => { + const attributes = { + "ai.toolCall.name": "calculate", + }; + + transformLLMSpans(attributes, "calculate.tool"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "execute_tool", + ); + }); + + it("should not set operation name for unknown span names", () => { + const attributes = {}; + + transformLLMSpans(attributes, "unknown.span"); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + undefined, + ); + }); + + it("should not set operation name when spanName is undefined", () => { + const attributes = {}; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + undefined, + ); + }); + }); + + describe("transformLLMSpans - provider name", () => { + it("should extract provider name from ai.model.provider", () => { + const attributes = { + "ai.model.provider": "openai.chat", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + "openai", + ); + assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + }); + + it("should extract provider name from complex provider string", () => { + const attributes = { + "ai.model.provider": "azure-openai.completions.v2", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + "azure-openai", + ); + assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Azure"); + }); + + it("should handle simple provider name without dots", () => { + const attributes = { + "ai.model.provider": "anthropic", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + "anthropic", + ); + assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Anthropic"); + }); + + it("should not set provider name when ai.model.provider is not present", () => { + const attributes = {}; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + undefined, + ); + }); + }); + + describe("transformLLMSpans - model id", () => { + it("should transform ai.model.id to gen_ai.request.model", () => { + const attributes = { + "ai.model.id": "gpt-4o", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.LLM_REQUEST_MODEL], + "gpt-4o", + ); + assert.strictEqual(attributes["ai.model.id"], undefined); + }); + + it("should not modify attributes when ai.model.id is not present", () => { + const attributes = { + someOtherAttr: "value", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.LLM_REQUEST_MODEL], + undefined, + ); + assert.strictEqual(attributes.someOtherAttr, "value"); + }); + }); + + describe("transformLLMSpans - finish reason", () => { + it("should transform ai.response.finishReason to array format", () => { + const attributes = { + "ai.response.finishReason": "stop", + }; + + transformLLMSpans(attributes); + + assert.deepStrictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + ["stop"], + ); + assert.strictEqual(attributes["ai.response.finishReason"], undefined); + }); + + it("should keep array if finishReason is already an array", () => { + const attributes = { + "ai.response.finishReason": ["stop", "length"], + }; + + transformLLMSpans(attributes); + + assert.deepStrictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + ["stop", "length"], + ); + assert.strictEqual(attributes["ai.response.finishReason"], undefined); + }); + + it("should handle different finish reason values", () => { + const finishReasons = ["stop", "length", "content_filter", "tool_calls"]; + + finishReasons.forEach((reason) => { + const attributes = { + "ai.response.finishReason": reason, + }; + + transformLLMSpans(attributes); + + assert.deepStrictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + [reason], + ); + }); + }); + + it("should not modify attributes when ai.response.finishReason is not present", () => { + const attributes = { + someOtherAttr: "value", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + undefined, + ); + }); + }); + + describe("transformLLMSpans - tool call attributes", () => { + it("should transform all tool call attributes to OpenTelemetry format", () => { + const attributes = { + "ai.toolCall.name": "getWeather", + "ai.toolCall.id": "call_abc123", + "ai.toolCall.args": '{"location":"San Francisco"}', + "ai.toolCall.result": '{"temperature":72}', + }; + + transformLLMSpans(attributes); + + // Check OpenTelemetry standard attributes + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_NAME], + "getWeather", + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], + "call_abc123", + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS], + '{"location":"San Francisco"}', + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_CALL_RESULT], + '{"temperature":72}', + ); + + // Check that tool call ID is deleted but args/result remain for Traceloop + assert.strictEqual(attributes["ai.toolCall.id"], undefined); + assert.strictEqual( + attributes["ai.toolCall.args"], + '{"location":"San Francisco"}', + ); + assert.strictEqual( + attributes["ai.toolCall.result"], + '{"temperature":72}', + ); + }); + + it("should handle tool call without ID", () => { + const attributes = { + "ai.toolCall.name": "calculate", + "ai.toolCall.args": '{"a":5,"b":3}', + "ai.toolCall.result": '{"result":8}', + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_NAME], + "calculate", + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], + undefined, + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS], + '{"a":5,"b":3}', + ); + }); + + it("should not modify attributes when no tool call attributes are present", () => { + const attributes = { + someOtherAttr: "value", + }; + + transformLLMSpans(attributes); + + assert.strictEqual(attributes[SpanAttributes.GEN_AI_TOOL_NAME], undefined); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], + undefined, + ); + assert.strictEqual(attributes.someOtherAttr, "value"); + }); + }); + + describe("transformLLMSpans - conversation id", () => { + it("should transform conversationId from metadata", () => { + const attributes = { + "ai.telemetry.metadata.conversationId": "conv_123", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + "conv_123", + ); + }); + + it("should use sessionId as fallback for conversation id", () => { + const attributes = { + "ai.telemetry.metadata.sessionId": "session_456", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + "session_456", + ); + }); + + it("should prefer conversationId over sessionId", () => { + const attributes = { + "ai.telemetry.metadata.conversationId": "conv_123", + "ai.telemetry.metadata.sessionId": "session_456", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + "conv_123", + ); + }); + + it("should not set conversation id when neither is present", () => { + const attributes = { + "ai.telemetry.metadata.userId": "user_789", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + undefined, + ); + }); + }); + + describe("transformLLMSpans - response metadata", () => { + it("should transform ai.response.model to gen_ai.response.model", () => { + const attributes = { + "ai.response.model": "gpt-4o-2024-05-13", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.LLM_RESPONSE_MODEL], + "gpt-4o-2024-05-13", + ); + assert.strictEqual(attributes["ai.response.model"], undefined); + }); + + it("should transform ai.response.id to gen_ai.response.id", () => { + const attributes = { + "ai.response.id": "chatcmpl-abc123", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + "chatcmpl-abc123", + ); + assert.strictEqual(attributes["ai.response.id"], undefined); + }); + + it("should transform both response.model and response.id", () => { + const attributes = { + "ai.response.model": "gpt-4o", + "ai.response.id": "chatcmpl-xyz789", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.LLM_RESPONSE_MODEL], + "gpt-4o", + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + "chatcmpl-xyz789", + ); + assert.strictEqual(attributes["ai.response.model"], undefined); + assert.strictEqual(attributes["ai.response.id"], undefined); + }); + + it("should not modify attributes when response metadata is not present", () => { + const attributes = { + someOtherAttr: "value", + }; + + transformLLMSpans(attributes); + + assert.strictEqual( + attributes[SpanAttributes.LLM_RESPONSE_MODEL], + undefined, + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + undefined, + ); + }); + }); + + describe("transformLLMSpans - complete transformation with all new attributes", () => { + it("should transform all new AI SDK attributes in a complete scenario", () => { + const attributes = { + "ai.model.id": "gpt-4o", + "ai.model.provider": "openai.chat", + "ai.response.text": "The weather in San Francisco is sunny!", + "ai.response.finishReason": "stop", + "ai.response.model": "gpt-4o-2024-05-13", + "ai.response.id": "chatcmpl-abc123", + "ai.prompt.messages": JSON.stringify([ + { role: "user", content: "What's the weather?" }, + ]), + "ai.usage.promptTokens": 10, + "ai.usage.completionTokens": 15, + "ai.telemetry.metadata.conversationId": "conv_456", + "ai.telemetry.metadata.userId": "user_789", + }; + + transformLLMSpans(attributes, "ai.generateText"); + + // Check operation name + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + "chat", + ); + + // Check model transformations + assert.strictEqual( + attributes[SpanAttributes.LLM_REQUEST_MODEL], + "gpt-4o", + ); + assert.strictEqual( + attributes[SpanAttributes.LLM_RESPONSE_MODEL], + "gpt-4o-2024-05-13", + ); + + // Check provider transformations + assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + "openai", + ); + + // Check response transformations + assert.deepStrictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + ["stop"], + ); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + "chatcmpl-abc123", + ); + + // Check conversation ID + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + "conv_456", + ); + + // Check that original AI SDK attributes are removed + assert.strictEqual(attributes["ai.model.id"], undefined); + assert.strictEqual(attributes["ai.model.provider"], undefined); + assert.strictEqual(attributes["ai.response.finishReason"], undefined); + assert.strictEqual(attributes["ai.response.model"], undefined); + assert.strictEqual(attributes["ai.response.id"], undefined); + + // Check metadata transformation + assert.strictEqual( + attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.userId` + ], + "user_789", + ); + }); + }); }); From ab0580a83704f5a5443e5b105ff9cf946e650b30 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:31:07 +0200 Subject: [PATCH 02/24] ai.prompts --- .../src/lib/tracing/ai-sdk-transformations.ts | 35 +++++++- .../test/ai-sdk-transformations.test.ts | 87 +++++++++++++++++++ 2 files changed, 121 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 b9597fcc..03d6a4f2 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -310,7 +310,40 @@ const transformPrompts = (attributes: Record): void => { if (AI_PROMPT in attributes) { try { const promptData = JSON.parse(attributes[AI_PROMPT] as string); - if (promptData.prompt && typeof promptData.prompt === "string") { + + // Handle case where promptData has a "messages" array + if (promptData.messages && Array.isArray(promptData.messages)) { + const messages = promptData.messages; + const inputMessages: any[] = []; + + messages.forEach((msg: { role: string; content: any }, index: number) => { + const processedContent = processMessageContent(msg.content); + const contentKey = `${SpanAttributes.LLM_PROMPTS}.${index}.content`; + attributes[contentKey] = processedContent; + attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = msg.role; + + // Add to OpenTelemetry standard gen_ai.input.messages format + inputMessages.push({ + role: msg.role, + parts: [ + { + type: TYPE_TEXT, + content: processedContent, + }, + ], + }); + }); + + // Set the OpenTelemetry standard input messages attribute + if (inputMessages.length > 0) { + attributes[SpanAttributes.LLM_INPUT_MESSAGES] = + JSON.stringify(inputMessages); + } + + delete attributes[AI_PROMPT]; + } + // Handle case where promptData has a "prompt" string + else if (promptData.prompt && typeof promptData.prompt === "string") { attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = promptData.prompt; attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = ROLE_USER; diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 76090492..1e47d217 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -471,6 +471,93 @@ describe("AI SDK Transformations", () => { assert.strictEqual(attributes.someOtherAttr, "value"); }); + it("should transform ai.prompt with messages array to prompt attributes", () => { + const promptData = { + messages: [{ role: "user", content: "What is the capital of France?" }], + }; + const attributes = { + "ai.prompt": JSON.stringify(promptData), + someOtherAttr: "value", + }; + + transformLLMSpans(attributes); + + // Check prompt attributes + assert.strictEqual( + attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + "What is the capital of France?", + ); + assert.strictEqual( + attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + "user", + ); + + // Check gen_ai.input.messages is set + assert.strictEqual( + typeof attributes[SpanAttributes.LLM_INPUT_MESSAGES], + "string", + ); + + const inputMessages = JSON.parse( + attributes[SpanAttributes.LLM_INPUT_MESSAGES], + ); + assert.strictEqual(inputMessages.length, 1); + assert.strictEqual(inputMessages[0].role, "user"); + assert.strictEqual(inputMessages[0].parts[0].type, "text"); + assert.strictEqual( + inputMessages[0].parts[0].content, + "What is the capital of France?", + ); + + // Check original attribute is removed + assert.strictEqual(attributes["ai.prompt"], undefined); + assert.strictEqual(attributes.someOtherAttr, "value"); + }); + + it("should transform ai.prompt with multiple messages", () => { + const promptData = { + messages: [ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "Hello!" }, + ], + }; + const attributes = { + "ai.prompt": JSON.stringify(promptData), + }; + + transformLLMSpans(attributes); + + // Check first message + assert.strictEqual( + attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + "You are a helpful assistant", + ); + assert.strictEqual( + attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + "system", + ); + + // Check second message + assert.strictEqual( + attributes[`${SpanAttributes.LLM_PROMPTS}.1.content`], + "Hello!", + ); + assert.strictEqual( + attributes[`${SpanAttributes.LLM_PROMPTS}.1.role`], + "user", + ); + + // Check gen_ai.input.messages + const inputMessages = JSON.parse( + attributes[SpanAttributes.LLM_INPUT_MESSAGES], + ); + assert.strictEqual(inputMessages.length, 2); + assert.strictEqual(inputMessages[0].role, "system"); + assert.strictEqual(inputMessages[1].role, "user"); + + assert.strictEqual(attributes["ai.prompt"], undefined); + }); + it("should not modify attributes when ai.prompt is not present", () => { const attributes = { someOtherAttr: "value", From 4047a13f1e7c030d26ef9daac828df22df59de77 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:58:55 +0200 Subject: [PATCH 03/24] pretty: --- .../src/lib/tracing/ai-sdk-transformations.ts | 42 ++++++++++--------- .../test/ai-sdk-transformations.test.ts | 9 ++-- 2 files changed, 27 insertions(+), 24 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 03d6a4f2..b82c498d 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -316,23 +316,26 @@ const transformPrompts = (attributes: Record): void => { const messages = promptData.messages; const inputMessages: any[] = []; - messages.forEach((msg: { role: string; content: any }, index: number) => { - const processedContent = processMessageContent(msg.content); - const contentKey = `${SpanAttributes.LLM_PROMPTS}.${index}.content`; - attributes[contentKey] = processedContent; - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = msg.role; - - // Add to OpenTelemetry standard gen_ai.input.messages format - inputMessages.push({ - role: msg.role, - parts: [ - { - type: TYPE_TEXT, - content: processedContent, - }, - ], - }); - }); + messages.forEach( + (msg: { role: string; content: any }, index: number) => { + const processedContent = processMessageContent(msg.content); + const contentKey = `${SpanAttributes.LLM_PROMPTS}.${index}.content`; + attributes[contentKey] = processedContent; + attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = + msg.role; + + // Add to OpenTelemetry standard gen_ai.input.messages format + inputMessages.push({ + role: msg.role, + parts: [ + { + type: TYPE_TEXT, + content: processedContent, + }, + ], + }); + }, + ); // Set the OpenTelemetry standard input messages attribute if (inputMessages.length > 0) { @@ -527,9 +530,7 @@ const transformFinishReason = (attributes: Record): void => { } }; -const transformToolCallAttributes = ( - attributes: Record, -): void => { +const transformToolCallAttributes = (attributes: Record): void => { // Transform tool name if ("ai.toolCall.name" in attributes) { attributes[SpanAttributes.GEN_AI_TOOL_NAME] = @@ -704,6 +705,7 @@ const transformToolCalls = (span: ReadableSpan): void => { const toolName = span.attributes["ai.toolCall.name"]; if (toolName) { span.attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] = toolName; + delete span.attributes["ai.toolCall.name"]; } } }; diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 1e47d217..093fb8d6 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -2301,7 +2301,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.GEN_AI_TOOL_NAME], undefined); + assert.strictEqual( + attributes[SpanAttributes.GEN_AI_TOOL_NAME], + undefined, + ); assert.strictEqual( attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], undefined, @@ -2500,9 +2503,7 @@ describe("AI SDK Transformations", () => { // Check metadata transformation assert.strictEqual( - attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.userId` - ], + attributes[`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.userId`], "user_789", ); }); From 55fbc8b3cb8daecd524820c62359f840d5d9c8da Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:35:16 +0200 Subject: [PATCH 04/24] added chatbot --- packages/sample-app/package.json | 1 + .../src/sample_chatbot_interactive.ts | 179 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 packages/sample-app/src/sample_chatbot_interactive.ts diff --git a/packages/sample-app/package.json b/packages/sample-app/package.json index 37ab8dd9..542417b9 100644 --- a/packages/sample-app/package.json +++ b/packages/sample-app/package.json @@ -42,6 +42,7 @@ "run:mcp": "npm run build && node dist/src/sample_mcp.js", "run:mcp:real": "npm run build && node dist/src/sample_mcp_real.js", "run:mcp:working": "npm run build && node dist/src/sample_mcp_working.js", + "run:chatbot": "npm run build && node dist/src/sample_chatbot_interactive.js", "dev:image_generation": "pnpm --filter @traceloop/instrumentation-openai build && pnpm --filter @traceloop/node-server-sdk build && npm run build && node dist/src/sample_openai_image_generation.js", "lint": "eslint .", "lint:fix": "eslint . --fix" diff --git a/packages/sample-app/src/sample_chatbot_interactive.ts b/packages/sample-app/src/sample_chatbot_interactive.ts new file mode 100644 index 00000000..d3c56b4e --- /dev/null +++ b/packages/sample-app/src/sample_chatbot_interactive.ts @@ -0,0 +1,179 @@ +import * as traceloop from "@traceloop/node-server-sdk"; +import { openai } from "@ai-sdk/openai"; +import { streamText, CoreMessage } from "ai"; +import * as readline from "readline"; + +import "dotenv/config"; + +traceloop.initialize({ + appName: "sample_chatbot_interactive", + disableBatch: true, +}); + +// ANSI color codes for terminal output +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + cyan: "\x1b[36m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", +}; + +class InteractiveChatbot { + private conversationHistory: CoreMessage[] = []; + private rl: readline.Interface; + + constructor() { + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`, + }); + } + + @traceloop.task({ name: "summarize_interaction" }) + async generateSummary( + userMessage: string, + assistantResponse: string, + ): Promise { + console.log( + `\n${colors.yellow}▼ SUMMARY${colors.reset} ${colors.dim}TASK${colors.reset}`, + ); + + const summaryResult = await streamText({ + model: openai("gpt-4o-mini"), + messages: [ + { + role: "system", + content: + "Create a very brief title (3-6 words) that summarizes this conversation exchange. Only return the title, nothing else.", + }, + { + role: "user", + content: `User: ${userMessage}\n\nAssistant: ${assistantResponse}`, + }, + ], + experimental_telemetry: { isEnabled: true }, + }); + + let summary = ""; + for await (const chunk of summaryResult.textStream) { + summary += chunk; + } + + const cleanSummary = summary.trim().replace(/^["']|["']$/g, ""); + console.log(`${colors.dim}${cleanSummary}${colors.reset}`); + + return cleanSummary; + } + + @traceloop.workflow({ name: "chat_interaction" }) + async processMessage(userMessage: string): Promise { + // Add user message to history + this.conversationHistory.push({ + role: "user", + content: userMessage, + }); + + console.log( + `\n${colors.green}${colors.bright}Assistant: ${colors.reset}`, + ); + + // Stream the response + const result = await streamText({ + model: openai("gpt-4o"), + messages: [ + { + role: "system", + content: + "You are a helpful AI assistant. Provide clear, concise, and friendly responses.", + }, + ...this.conversationHistory, + ], + experimental_telemetry: { isEnabled: true }, + }); + + let fullResponse = ""; + for await (const chunk of result.textStream) { + process.stdout.write(chunk); + fullResponse += chunk; + } + + console.log("\n"); + + // Add assistant response to history + this.conversationHistory.push({ + role: "assistant", + content: fullResponse, + }); + + // Generate summary for this interaction + await this.generateSummary(userMessage, fullResponse); + + return fullResponse; + } + + clearHistory(): void { + this.conversationHistory = []; + console.log( + `\n${colors.magenta}✓ Conversation history cleared${colors.reset}\n`, + ); + } + + async start(): Promise { + console.log(`${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗`); + console.log(`║ Interactive AI Chatbot with Traceloop ║`); + console.log(`╚════════════════════════════════════════════════════════════╝${colors.reset}\n`); + console.log(`${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`); + + this.rl.prompt(); + + this.rl.on("line", async (input: string) => { + const trimmedInput = input.trim(); + + if (!trimmedInput) { + this.rl.prompt(); + return; + } + + if (trimmedInput === "/exit") { + console.log( + `\n${colors.magenta}Goodbye! 👋${colors.reset}\n`, + ); + this.rl.close(); + process.exit(0); + } + + if (trimmedInput === "/clear") { + this.clearHistory(); + this.rl.prompt(); + return; + } + + try { + await this.processMessage(trimmedInput); + } catch (error) { + console.error( + `\n${colors.bright}Error:${colors.reset} ${error instanceof Error ? error.message : String(error)}\n`, + ); + } + + this.rl.prompt(); + }); + + this.rl.on("close", () => { + console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`); + process.exit(0); + }); + } +} + +async function main() { + const chatbot = new InteractiveChatbot(); + await chatbot.start(); +} + +main().catch(console.error); From 3429596e3f1e97221fc673215b3bf35863a919d7 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:12:48 +0200 Subject: [PATCH 05/24] change --- packages/ai-semantic-conventions/package.json | 2 +- .../src/SemanticAttributes.ts | 98 ++++++++++++++----- .../src/lib/tracing/ai-sdk-transformations.ts | 12 +-- pnpm-lock.yaml | 52 ++++++++-- 4 files changed, 125 insertions(+), 39 deletions(-) diff --git a/packages/ai-semantic-conventions/package.json b/packages/ai-semantic-conventions/package.json index 02f80da0..a6a3e419 100644 --- a/packages/ai-semantic-conventions/package.json +++ b/packages/ai-semantic-conventions/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/semantic-conventions": "^1.36.0" + "@opentelemetry/semantic-conventions": "^1.38.0" }, "homepage": "https://github.com/traceloop/openllmetry-js/tree/main/packages/ai-semantic-conventions", "gitHead": "ef1e70d6037f7b5c061056ef2be16e3f55f02ed5" diff --git a/packages/ai-semantic-conventions/src/SemanticAttributes.ts b/packages/ai-semantic-conventions/src/SemanticAttributes.ts index 4d0b049b..07da4f95 100644 --- a/packages/ai-semantic-conventions/src/SemanticAttributes.ts +++ b/packages/ai-semantic-conventions/src/SemanticAttributes.ts @@ -15,41 +15,89 @@ */ import { + ATTR_GEN_AI_AGENT_NAME, + ATTR_GEN_AI_COMPLETION, + ATTR_GEN_AI_CONVERSATION_ID, + ATTR_GEN_AI_INPUT_MESSAGES, + ATTR_GEN_AI_OPERATION_NAME, + ATTR_GEN_AI_OUTPUT_MESSAGES, + ATTR_GEN_AI_PROMPT, + ATTR_GEN_AI_PROVIDER_NAME, + ATTR_GEN_AI_REQUEST_MAX_TOKENS, + ATTR_GEN_AI_REQUEST_MODEL, + ATTR_GEN_AI_REQUEST_TEMPERATURE, + ATTR_GEN_AI_REQUEST_TOP_P, + ATTR_GEN_AI_RESPONSE_FINISH_REASONS, + ATTR_GEN_AI_RESPONSE_ID, + ATTR_GEN_AI_RESPONSE_MODEL, + ATTR_GEN_AI_SYSTEM, + ATTR_GEN_AI_TOOL_CALL_ARGUMENTS, + ATTR_GEN_AI_TOOL_CALL_ID, + ATTR_GEN_AI_TOOL_CALL_RESULT, + ATTR_GEN_AI_TOOL_NAME, + ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, + ATTR_GEN_AI_USAGE_PROMPT_TOKENS, // @ts-expect-error - Using exports path that TypeScript doesn't recognize but works at runtime } from "@opentelemetry/semantic-conventions/incubating"; export const SpanAttributes = { - LLM_SYSTEM: "gen_ai.system", - LLM_REQUEST_MODEL: "gen_ai.request.model", - LLM_REQUEST_MAX_TOKENS: "gen_ai.request.max_tokens", - LLM_REQUEST_TEMPERATURE: "gen_ai.request.temperature", - LLM_REQUEST_TOP_P: "gen_ai.request.top_p", - LLM_PROMPTS: "gen_ai.prompt", - LLM_COMPLETIONS: "gen_ai.completion", - LLM_INPUT_MESSAGES: "gen_ai.input.messages", - LLM_OUTPUT_MESSAGES: "gen_ai.output.messages", - LLM_RESPONSE_MODEL: "gen_ai.response.model", - LLM_USAGE_PROMPT_TOKENS: "gen_ai.usage.prompt_tokens", - LLM_USAGE_COMPLETION_TOKENS: "gen_ai.usage.completion_tokens", - LLM_USAGE_INPUT_TOKENS: ATTR_GEN_AI_USAGE_INPUT_TOKENS, - LLM_USAGE_OUTPUT_TOKENS: ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, + ATTR_GEN_AI_SYSTEM, + ATTR_GEN_AI_REQUEST_MODEL, + ATTR_GEN_AI_REQUEST_MAX_TOKENS, + ATTR_GEN_AI_REQUEST_TEMPERATURE, + ATTR_GEN_AI_REQUEST_TOP_P, + ATTR_GEN_AI_PROMPT, + ATTR_GEN_AI_COMPLETION, + ATTR_GEN_AI_INPUT_MESSAGES, + ATTR_GEN_AI_OUTPUT_MESSAGES, + ATTR_GEN_AI_RESPONSE_MODEL, + ATTR_GEN_AI_USAGE_PROMPT_TOKENS, + ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, + ATTR_GEN_AI_USAGE_INPUT_TOKENS, + ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, + ATTR_GEN_AI_OPERATION_NAME, + ATTR_GEN_AI_PROVIDER_NAME, + ATTR_GEN_AI_TOOL_NAME, + ATTR_GEN_AI_TOOL_CALL_ID, + ATTR_GEN_AI_TOOL_CALL_ARGUMENTS, + ATTR_GEN_AI_TOOL_CALL_RESULT, + ATTR_GEN_AI_RESPONSE_ID, + ATTR_GEN_AI_RESPONSE_FINISH_REASONS, + ATTR_GEN_AI_CONVERSATION_ID, + ATTR_GEN_AI_AGENT_NAME, + + // Attributes not yet in @opentelemetry/semantic-conventions GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS: "gen_ai.usage.cache_creation_input_tokens", GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: "gen_ai.usage.cache_read_input_tokens", GEN_AI_USAGE_REASONING_TOKENS: "gen_ai.usage.reasoning_tokens", - GEN_AI_OPERATION_NAME: "gen_ai.operation.name", - GEN_AI_PROVIDER_NAME: "gen_ai.provider.name", - GEN_AI_TOOL_NAME: "gen_ai.tool.name", - GEN_AI_TOOL_CALL_ID: "gen_ai.tool.call.id", - GEN_AI_TOOL_CALL_ARGUMENTS: "gen_ai.tool.call.arguments", - GEN_AI_TOOL_CALL_RESULT: "gen_ai.tool.call.result", - GEN_AI_RESPONSE_ID: "gen_ai.response.id", - GEN_AI_RESPONSE_FINISH_REASONS: "gen_ai.response.finish_reasons", - GEN_AI_CONVERSATION_ID: "gen_ai.conversation.id", - - GEN_AI_AGENT_NAME: "gen_ai.agent.name", + + // Legacy aliases for backward compatibility + LLM_SYSTEM: ATTR_GEN_AI_SYSTEM, + LLM_REQUEST_MODEL: ATTR_GEN_AI_REQUEST_MODEL, + LLM_REQUEST_MAX_TOKENS: ATTR_GEN_AI_REQUEST_MAX_TOKENS, + LLM_REQUEST_TEMPERATURE: ATTR_GEN_AI_REQUEST_TEMPERATURE, + LLM_REQUEST_TOP_P: ATTR_GEN_AI_REQUEST_TOP_P, + LLM_PROMPTS: ATTR_GEN_AI_PROMPT, + LLM_COMPLETIONS: ATTR_GEN_AI_COMPLETION, + LLM_INPUT_MESSAGES: ATTR_GEN_AI_INPUT_MESSAGES, + LLM_OUTPUT_MESSAGES: ATTR_GEN_AI_OUTPUT_MESSAGES, + LLM_RESPONSE_MODEL: ATTR_GEN_AI_RESPONSE_MODEL, + LLM_USAGE_PROMPT_TOKENS: ATTR_GEN_AI_USAGE_PROMPT_TOKENS, + LLM_USAGE_COMPLETION_TOKENS: ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, + GEN_AI_OPERATION_NAME: ATTR_GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME: ATTR_GEN_AI_PROVIDER_NAME, + GEN_AI_TOOL_NAME: ATTR_GEN_AI_TOOL_NAME, + GEN_AI_TOOL_CALL_ID: ATTR_GEN_AI_TOOL_CALL_ID, + GEN_AI_TOOL_CALL_ARGUMENTS: ATTR_GEN_AI_TOOL_CALL_ARGUMENTS, + GEN_AI_TOOL_CALL_RESULT: ATTR_GEN_AI_TOOL_CALL_RESULT, + GEN_AI_RESPONSE_ID: ATTR_GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_FINISH_REASONS: ATTR_GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_CONVERSATION_ID: ATTR_GEN_AI_CONVERSATION_ID, + GEN_AI_AGENT_NAME: ATTR_GEN_AI_AGENT_NAME, + // LLM LLM_REQUEST_TYPE: "llm.request.type", 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 b82c498d..e437659f 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -375,10 +375,10 @@ const transformPrompts = (attributes: Record): void => { const transformPromptTokens = (attributes: Record): void => { // Make sure we have the right naming convention if ( - !(SpanAttributes.LLM_USAGE_INPUT_TOKENS in attributes) && + !(SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS in attributes) && AI_USAGE_PROMPT_TOKENS in attributes ) { - attributes[SpanAttributes.LLM_USAGE_INPUT_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS] = attributes[AI_USAGE_PROMPT_TOKENS]; } @@ -390,10 +390,10 @@ const transformPromptTokens = (attributes: Record): void => { const transformCompletionTokens = (attributes: Record): void => { // Make sure we have the right naming convention if ( - !(SpanAttributes.LLM_USAGE_OUTPUT_TOKENS in attributes) && + !(SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS in attributes) && AI_USAGE_COMPLETION_TOKENS in attributes ) { - attributes[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = attributes[AI_USAGE_COMPLETION_TOKENS]; } @@ -452,8 +452,8 @@ const transformProviderMetadata = (attributes: Record): void => { }; const calculateTotalTokens = (attributes: Record): void => { - const inputTokens = attributes[SpanAttributes.LLM_USAGE_INPUT_TOKENS]; - const outputTokens = attributes[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS]; + const inputTokens = attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]; + const outputTokens = attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]; if (inputTokens && outputTokens) { attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`] = diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abdaf00f..f00ac5d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,8 +94,8 @@ importers: specifier: ^1.9.0 version: 1.9.0 '@opentelemetry/semantic-conventions': - specifier: ^1.36.0 - version: 1.36.0 + specifier: ^1.38.0 + version: 1.38.0 packages/instrumentation-anthropic: dependencies: @@ -313,7 +313,7 @@ importers: devDependencies: '@langchain/community': specifier: ^0.3.50 - version: 0.3.51(ff0bc764bdeb7c31ee61157b93391bc7) + version: 0.3.51(3911da0cb6fab4207141c54267caa33d) '@langchain/openai': specifier: ^0.6.2 version: 0.6.2(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) @@ -743,7 +743,7 @@ importers: version: 0.1.13(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76))) '@langchain/community': specifier: ^0.3.50 - version: 0.3.51(ff0bc764bdeb7c31ee61157b93391bc7) + version: 0.3.51(3911da0cb6fab4207141c54267caa33d) '@langchain/core': specifier: ^0.3.70 version: 0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.76)) @@ -3438,6 +3438,10 @@ packages: resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.38.0': + resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + engines: {node: '>=14'} + '@phenomnomnominal/tsquery@5.0.1': resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==} peerDependencies: @@ -6147,6 +6151,10 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + just-diff-apply@5.5.0: resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} @@ -6165,6 +6173,9 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7537,6 +7548,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -10495,7 +10511,7 @@ snapshots: transitivePeerDependencies: - aws-crt - '@langchain/community@0.3.51(ff0bc764bdeb7c31ee61157b93391bc7)': + '@langchain/community@0.3.51(3911da0cb6fab4207141c54267caa33d)': dependencies: '@browserbasehq/stagehand': 1.14.0(@playwright/test@1.54.1)(deepmerge@4.3.1)(dotenv@17.2.1)(encoding@0.1.13)(openai@5.12.2(ws@8.18.3)(zod@3.25.76))(zod@3.25.76) '@ibm-cloud/watsonx-ai': 1.6.8 @@ -10530,7 +10546,7 @@ snapshots: hnswlib-node: 3.0.0 html-to-text: 9.0.5 ignore: 5.3.2 - jsonwebtoken: 9.0.2 + jsonwebtoken: 9.0.3 lodash: 4.17.21 playwright: 1.54.1 weaviate-client: 3.7.0(encoding@0.1.13) @@ -11603,6 +11619,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.36.0': {} + '@opentelemetry/semantic-conventions@1.38.0': {} + '@phenomnomnominal/tsquery@5.0.1(typescript@5.9.3)': dependencies: esquery: 1.6.0 @@ -14361,7 +14379,7 @@ snapshots: file-type: 16.5.4 form-data: 4.0.5 isstream: 0.1.2 - jsonwebtoken: 9.0.2 + jsonwebtoken: 9.0.3 mime-types: 2.1.35 retry-axios: 2.6.0(axios@1.13.2) tough-cookie: 4.1.4 @@ -14730,6 +14748,19 @@ snapshots: ms: 2.1.3 semver: 7.7.2 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + just-diff-apply@5.5.0: {} just-diff@6.0.2: {} @@ -14756,6 +14787,11 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -16396,6 +16432,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: {} + send@0.19.0(supports-color@10.0.0): dependencies: debug: 2.6.9(supports-color@10.0.0) From 5754d4c5e762161abe05cd7ee809ae807d596f0e Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:25:10 +0200 Subject: [PATCH 06/24] fix test --- .../src/SemanticAttributes.ts | 25 -- .../src/lib/tracing/ai-sdk-transformations.ts | 74 ++--- .../test/ai-sdk-transformations.test.ts | 300 +++++++++--------- 3 files changed, 187 insertions(+), 212 deletions(-) diff --git a/packages/ai-semantic-conventions/src/SemanticAttributes.ts b/packages/ai-semantic-conventions/src/SemanticAttributes.ts index 07da4f95..a5603001 100644 --- a/packages/ai-semantic-conventions/src/SemanticAttributes.ts +++ b/packages/ai-semantic-conventions/src/SemanticAttributes.ts @@ -74,31 +74,6 @@ export const SpanAttributes = { GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: "gen_ai.usage.cache_read_input_tokens", GEN_AI_USAGE_REASONING_TOKENS: "gen_ai.usage.reasoning_tokens", - // Legacy aliases for backward compatibility - LLM_SYSTEM: ATTR_GEN_AI_SYSTEM, - LLM_REQUEST_MODEL: ATTR_GEN_AI_REQUEST_MODEL, - LLM_REQUEST_MAX_TOKENS: ATTR_GEN_AI_REQUEST_MAX_TOKENS, - LLM_REQUEST_TEMPERATURE: ATTR_GEN_AI_REQUEST_TEMPERATURE, - LLM_REQUEST_TOP_P: ATTR_GEN_AI_REQUEST_TOP_P, - LLM_PROMPTS: ATTR_GEN_AI_PROMPT, - LLM_COMPLETIONS: ATTR_GEN_AI_COMPLETION, - LLM_INPUT_MESSAGES: ATTR_GEN_AI_INPUT_MESSAGES, - LLM_OUTPUT_MESSAGES: ATTR_GEN_AI_OUTPUT_MESSAGES, - LLM_RESPONSE_MODEL: ATTR_GEN_AI_RESPONSE_MODEL, - LLM_USAGE_PROMPT_TOKENS: ATTR_GEN_AI_USAGE_PROMPT_TOKENS, - LLM_USAGE_COMPLETION_TOKENS: ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, - GEN_AI_OPERATION_NAME: ATTR_GEN_AI_OPERATION_NAME, - GEN_AI_PROVIDER_NAME: ATTR_GEN_AI_PROVIDER_NAME, - GEN_AI_TOOL_NAME: ATTR_GEN_AI_TOOL_NAME, - GEN_AI_TOOL_CALL_ID: ATTR_GEN_AI_TOOL_CALL_ID, - GEN_AI_TOOL_CALL_ARGUMENTS: ATTR_GEN_AI_TOOL_CALL_ARGUMENTS, - GEN_AI_TOOL_CALL_RESULT: ATTR_GEN_AI_TOOL_CALL_RESULT, - GEN_AI_RESPONSE_ID: ATTR_GEN_AI_RESPONSE_ID, - GEN_AI_RESPONSE_FINISH_REASONS: ATTR_GEN_AI_RESPONSE_FINISH_REASONS, - GEN_AI_CONVERSATION_ID: ATTR_GEN_AI_CONVERSATION_ID, - GEN_AI_AGENT_NAME: ATTR_GEN_AI_AGENT_NAME, - - // LLM LLM_REQUEST_TYPE: "llm.request.type", LLM_USAGE_TOTAL_TOKENS: "llm.usage.total_tokens", 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 e437659f..b6deb23f 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -73,9 +73,9 @@ const getAgentNameFromAttributes = ( const transformResponseText = (attributes: Record): void => { if (AI_RESPONSE_TEXT in attributes) { - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = attributes[AI_RESPONSE_TEXT]; - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = ROLE_ASSISTANT; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = ROLE_ASSISTANT; const outputMessage = { role: ROLE_ASSISTANT, @@ -86,7 +86,7 @@ const transformResponseText = (attributes: Record): void => { }, ], }; - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] = JSON.stringify([ + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES] = JSON.stringify([ outputMessage, ]); @@ -96,9 +96,9 @@ const transformResponseText = (attributes: Record): void => { const transformResponseObject = (attributes: Record): void => { if (AI_RESPONSE_OBJECT in attributes) { - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = attributes[AI_RESPONSE_OBJECT]; - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = ROLE_ASSISTANT; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = ROLE_ASSISTANT; const outputMessage = { role: ROLE_ASSISTANT, @@ -109,7 +109,7 @@ const transformResponseObject = (attributes: Record): void => { }, ], }; - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] = JSON.stringify([ + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES] = JSON.stringify([ outputMessage, ]); @@ -124,16 +124,16 @@ const transformResponseToolCalls = (attributes: Record): void => { attributes[AI_RESPONSE_TOOL_CALLS] as string, ); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = ROLE_ASSISTANT; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = ROLE_ASSISTANT; const toolCallParts: any[] = []; toolCalls.forEach((toolCall: any, index: number) => { if (toolCall.toolCallType === "function") { attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.${index}.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.${index}.name` ] = toolCall.toolName; attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.${index}.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.${index}.arguments` ] = toolCall.args; toolCallParts.push({ @@ -151,7 +151,7 @@ const transformResponseToolCalls = (attributes: Record): void => { role: ROLE_ASSISTANT, parts: toolCallParts, }; - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] = JSON.stringify([ + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES] = JSON.stringify([ outputMessage, ]); } @@ -279,9 +279,9 @@ const transformPrompts = (attributes: Record): void => { messages.forEach((msg: { role: string; content: any }, index: number) => { const processedContent = processMessageContent(msg.content); - const contentKey = `${SpanAttributes.LLM_PROMPTS}.${index}.content`; + const contentKey = `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`; attributes[contentKey] = processedContent; - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = msg.role; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; // Add to OpenTelemetry standard gen_ai.input.messages format inputMessages.push({ @@ -297,7 +297,7 @@ const transformPrompts = (attributes: Record): void => { // Set the OpenTelemetry standard input messages attribute if (inputMessages.length > 0) { - attributes[SpanAttributes.LLM_INPUT_MESSAGES] = + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify(inputMessages); } @@ -319,9 +319,9 @@ const transformPrompts = (attributes: Record): void => { messages.forEach( (msg: { role: string; content: any }, index: number) => { const processedContent = processMessageContent(msg.content); - const contentKey = `${SpanAttributes.LLM_PROMPTS}.${index}.content`; + const contentKey = `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`; attributes[contentKey] = processedContent; - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; // Add to OpenTelemetry standard gen_ai.input.messages format @@ -339,7 +339,7 @@ const transformPrompts = (attributes: Record): void => { // Set the OpenTelemetry standard input messages attribute if (inputMessages.length > 0) { - attributes[SpanAttributes.LLM_INPUT_MESSAGES] = + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify(inputMessages); } @@ -347,9 +347,9 @@ const transformPrompts = (attributes: Record): void => { } // Handle case where promptData has a "prompt" string else if (promptData.prompt && typeof promptData.prompt === "string") { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = promptData.prompt; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = ROLE_USER; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = ROLE_USER; const inputMessage = { role: ROLE_USER, @@ -360,7 +360,7 @@ const transformPrompts = (attributes: Record): void => { }, ], }; - attributes[SpanAttributes.LLM_INPUT_MESSAGES] = JSON.stringify([ + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify([ inputMessage, ]); @@ -384,7 +384,7 @@ const transformPromptTokens = (attributes: Record): void => { // Clean up legacy attributes delete attributes[AI_USAGE_PROMPT_TOKENS]; - delete attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS]; + delete attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]; }; const transformCompletionTokens = (attributes: Record): void => { @@ -399,7 +399,7 @@ const transformCompletionTokens = (attributes: Record): void => { // Clean up legacy attributes delete attributes[AI_USAGE_COMPLETION_TOKENS]; - delete attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]; + delete attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]; }; const transformProviderMetadata = (attributes: Record): void => { @@ -470,7 +470,7 @@ const transformVendor = (attributes: Record): void => { if (typeof vendor === "string" && vendor.length > 0) { // Extract base provider name for OpenTelemetry standard (e.g., "openai" from "openai.chat") const providerName = vendor.split(".")[0]; - attributes[SpanAttributes.GEN_AI_PROVIDER_NAME] = providerName; + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = providerName; for (const prefix of Object.keys(VENDOR_MAPPING)) { if (vendor.startsWith(prefix)) { @@ -480,7 +480,7 @@ const transformVendor = (attributes: Record): void => { } } - attributes[SpanAttributes.LLM_SYSTEM] = mappedVendor || vendor; + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = mappedVendor || vendor; delete attributes[AI_MODEL_PROVIDER]; } }; @@ -504,14 +504,14 @@ const transformOperationName = ( } if (operationName) { - attributes[SpanAttributes.GEN_AI_OPERATION_NAME] = operationName; + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME] = operationName; } }; const transformModelId = (attributes: Record): void => { const AI_MODEL_ID = "ai.model.id"; if (AI_MODEL_ID in attributes) { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = attributes[AI_MODEL_ID]; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = attributes[AI_MODEL_ID]; delete attributes[AI_MODEL_ID]; } }; @@ -521,7 +521,7 @@ const transformFinishReason = (attributes: Record): void => { if (AI_RESPONSE_FINISH_REASON in attributes) { const finishReason = attributes[AI_RESPONSE_FINISH_REASON]; // Convert to array format for OTel standard - attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS] = Array.isArray( + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS] = Array.isArray( finishReason, ) ? finishReason @@ -533,28 +533,28 @@ const transformFinishReason = (attributes: Record): void => { const transformToolCallAttributes = (attributes: Record): void => { // Transform tool name if ("ai.toolCall.name" in attributes) { - attributes[SpanAttributes.GEN_AI_TOOL_NAME] = + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_NAME] = attributes["ai.toolCall.name"]; // Keep ai.toolCall.name for now, will be deleted in transformToolCalls } // Transform tool call ID if ("ai.toolCall.id" in attributes) { - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID] = + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ID] = attributes["ai.toolCall.id"]; delete attributes["ai.toolCall.id"]; } // Transform tool arguments (keep both OTel and Traceloop versions) if ("ai.toolCall.args" in attributes) { - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS] = + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ARGUMENTS] = attributes["ai.toolCall.args"]; // Don't delete yet - transformToolCalls will handle entity input/output } // Transform tool result (keep both OTel and Traceloop versions) if ("ai.toolCall.result" in attributes) { - attributes[SpanAttributes.GEN_AI_TOOL_CALL_RESULT] = + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_RESULT] = attributes["ai.toolCall.result"]; // Don't delete yet - transformToolCalls will handle entity input/output } @@ -566,9 +566,9 @@ const transformConversationId = (attributes: Record): void => { const sessionId = attributes["ai.telemetry.metadata.sessionId"]; if (conversationId) { - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID] = conversationId; + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID] = conversationId; } else if (sessionId) { - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID] = sessionId; + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID] = sessionId; } }; @@ -577,13 +577,13 @@ const transformResponseMetadata = (attributes: Record): void => { const AI_RESPONSE_ID = "ai.response.id"; if (AI_RESPONSE_MODEL in attributes) { - attributes[SpanAttributes.LLM_RESPONSE_MODEL] = + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL] = attributes[AI_RESPONSE_MODEL]; delete attributes[AI_RESPONSE_MODEL]; } if (AI_RESPONSE_ID in attributes) { - attributes[SpanAttributes.GEN_AI_RESPONSE_ID] = attributes[AI_RESPONSE_ID]; + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID] = attributes[AI_RESPONSE_ID]; delete attributes[AI_RESPONSE_ID]; } }; @@ -619,7 +619,7 @@ const transformTelemetryMetadata = ( } if (agentName) { - attributes[SpanAttributes.GEN_AI_AGENT_NAME] = agentName; + attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME] = agentName; const topLevelSpanNames = [ AI_GENERATE_TEXT, @@ -636,8 +636,8 @@ const transformTelemetryMetadata = ( TraceloopSpanKindValues.AGENT; attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] = agentName; - const inputMessages = attributes[SpanAttributes.LLM_INPUT_MESSAGES]; - const outputMessages = attributes[SpanAttributes.LLM_OUTPUT_MESSAGES]; + const inputMessages = attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES]; + const outputMessages = attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES]; const toolArgs = attributes["ai.toolCall.args"]; const toolResult = attributes["ai.toolCall.result"]; diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 093fb8d6..6231f12b 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -48,11 +48,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "Hello, how can I help you?", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual(attributes["ai.response.text"], undefined); @@ -78,11 +78,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual(attributes["ai.response.text"], undefined); @@ -99,11 +99,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], '{"filteredText":"Hello","changesApplied":false}', ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual(attributes["ai.response.object"], undefined); @@ -148,30 +148,30 @@ describe("AI SDK Transformations", () => { // Check that role is set assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); // Check first tool call assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.name`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.name`], "getWeather", ); assert.strictEqual( attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.arguments` ], '{"location": "San Francisco"}', ); // Check second tool call assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.1.name`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.name`], "searchRestaurants", ); assert.strictEqual( attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.1.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.arguments` ], '{"city": "San Francisco"}', ); @@ -219,19 +219,19 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "You are a helpful assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "system", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.1.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.content`], "Hello", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.1.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.role`], "user", ); assert.strictEqual(attributes["ai.prompt.messages"], undefined); @@ -251,11 +251,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What's in this image?", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); }); @@ -280,11 +280,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Help me plan a trip to San Francisco. I'd like to know about the weather and restaurants.", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); }); @@ -307,11 +307,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What's in this image? Please describe it.", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); }); @@ -331,11 +331,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Help me plan a trip to San Francisco. What should I know about the weather?", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); }); @@ -356,11 +356,11 @@ describe("AI SDK Transformations", () => { // Should preserve the original JSON since it's not simple text assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], '[{"type":"tool-call","id":"call_123","name":"getWeather","args":{"location":"Paris"}}]', ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "assistant", ); }); @@ -381,11 +381,11 @@ describe("AI SDK Transformations", () => { // Should preserve the original JSON since it has mixed content assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], '[{"type":"text","text":"What\'s the weather?"},{"type":"image","url":"data:image/jpeg;base64,..."}]', ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); }); @@ -432,7 +432,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - const result = attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`]; + const result = attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]; // The escape sequences should be properly unescaped assert.strictEqual( @@ -440,7 +440,7 @@ describe("AI SDK Transformations", () => { "Help me plan a trip to San Francisco. I'd like to know:\n1. What's the weather like there?\n2. Find some good restaurants to try\n3. If I'm traveling from New York, how far is it?\n\nPlease use the available tools to get current information and provide a comprehensive travel guide.", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); }); @@ -460,11 +460,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Help me plan a trip to San Francisco. I\\'d like to know:\\n1. What\\'s the weather like there?\\n2. Find some restaurants\\n\\nPlease help!", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual(attributes["ai.prompt"], undefined); @@ -484,22 +484,22 @@ describe("AI SDK Transformations", () => { // Check prompt attributes assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What is the capital of France?", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); // Check gen_ai.input.messages is set assert.strictEqual( - typeof attributes[SpanAttributes.LLM_INPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], "string", ); const inputMessages = JSON.parse( - attributes[SpanAttributes.LLM_INPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], ); assert.strictEqual(inputMessages.length, 1); assert.strictEqual(inputMessages[0].role, "user"); @@ -529,27 +529,27 @@ describe("AI SDK Transformations", () => { // Check first message assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "You are a helpful assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "system", ); // Check second message assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.1.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.content`], "Hello!", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.1.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.role`], "user", ); // Check gen_ai.input.messages const inputMessages = JSON.parse( - attributes[SpanAttributes.LLM_INPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], ); assert.strictEqual(inputMessages.length, 2); assert.strictEqual(inputMessages[0].role, "system"); @@ -619,7 +619,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name`], + attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name`], "getWeather", ); assert.strictEqual( @@ -884,10 +884,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_INPUT_TOKENS], 50); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 50); assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], undefined, ); assert.strictEqual(attributes.someOtherAttr, "value"); @@ -913,7 +913,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_INPUT_TOKENS], 0); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 0); assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); }); }); @@ -930,12 +930,12 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 25, ); assert.strictEqual(attributes["ai.usage.completionTokens"], undefined); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], undefined, ); assert.strictEqual(attributes.someOtherAttr, "value"); @@ -961,7 +961,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS], 0); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 0); assert.strictEqual(attributes["ai.usage.completionTokens"], undefined); }); }); @@ -969,8 +969,8 @@ describe("AI SDK Transformations", () => { describe("transformAiSdkAttributes - total tokens calculation", () => { it("should calculate total tokens from input and output tokens", () => { const attributes = { - [SpanAttributes.LLM_USAGE_INPUT_TOKENS]: 50, - [SpanAttributes.LLM_USAGE_OUTPUT_TOKENS]: 25, + [SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 50, + [SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 25, }; transformLLMSpans(attributes); @@ -980,8 +980,8 @@ describe("AI SDK Transformations", () => { it("should handle string token values", () => { const attributes = { - [SpanAttributes.LLM_USAGE_INPUT_TOKENS]: "50", - [SpanAttributes.LLM_USAGE_OUTPUT_TOKENS]: "25", + [SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: "50", + [SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: "25", }; transformLLMSpans(attributes); @@ -1004,7 +1004,7 @@ describe("AI SDK Transformations", () => { it("should not calculate total when output tokens are missing", () => { const attributes = { - [SpanAttributes.LLM_USAGE_INPUT_TOKENS]: 50, + [SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 50, }; transformLLMSpans(attributes); @@ -1036,7 +1036,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); assert.strictEqual(attributes["ai.model.provider"], undefined); assert.strictEqual(attributes.someOtherAttr, "value"); }); @@ -1055,7 +1055,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); assert.strictEqual(attributes["ai.model.provider"], undefined); }); }); @@ -1070,7 +1070,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Azure"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); assert.strictEqual(attributes["ai.model.provider"], undefined); }); }); @@ -1082,7 +1082,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Anthropic"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Anthropic"); assert.strictEqual(attributes["ai.model.provider"], undefined); }); @@ -1104,7 +1104,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], ""); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], ""); assert.strictEqual(attributes["ai.model.provider"], undefined); }); }); @@ -1126,31 +1126,31 @@ describe("AI SDK Transformations", () => { // Check response text transformation assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "Hello!", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); // Check prompt messages transformation assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Hi", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); // Check token transformations - should keep input/output tokens - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_INPUT_TOKENS], 10); - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS], 5); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 5); assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); // Check vendor transformation - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); // Check original AI SDK attributes are removed assert.strictEqual(attributes["ai.response.text"], undefined); @@ -1158,11 +1158,11 @@ describe("AI SDK Transformations", () => { assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); assert.strictEqual(attributes["ai.usage.completionTokens"], undefined); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], undefined, ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], undefined, ); assert.strictEqual(attributes["ai.model.provider"], undefined); @@ -1180,7 +1180,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "Hello!", ); assert.strictEqual(attributes.someOtherAttr, "value"); @@ -1202,31 +1202,31 @@ describe("AI SDK Transformations", () => { // Check response object transformation assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], '{"result":"Hello!"}', ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); // Check prompt messages transformation assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Hi", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); // Check token transformations - should keep input/output tokens - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_INPUT_TOKENS], 10); - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS], 5); - assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 5); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_TOTAL_TOKENS], 15); // Check vendor transformation - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Azure"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); // Check original AI SDK attributes are removed assert.strictEqual(attributes["ai.response.object"], undefined); @@ -1234,11 +1234,11 @@ describe("AI SDK Transformations", () => { assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); assert.strictEqual(attributes["ai.usage.completionTokens"], undefined); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], undefined, ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], undefined, ); assert.strictEqual(attributes["ai.model.provider"], undefined); @@ -1272,15 +1272,15 @@ describe("AI SDK Transformations", () => { // Check tools transformation assert.strictEqual( - attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name`], + attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name`], "getWeather", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.description`], + attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.description`], "Get weather for a location", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.parameters`], + attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.parameters`], JSON.stringify({ type: "object", properties: { location: { type: "string" } }, @@ -1289,11 +1289,11 @@ describe("AI SDK Transformations", () => { // Check other transformations still work assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "I'll help you with that!", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Get weather", ); assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 23); @@ -1323,12 +1323,12 @@ describe("AI SDK Transformations", () => { // Check that gen_ai.input.messages is properly set assert.strictEqual( - typeof attributes[SpanAttributes.LLM_INPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], "string", ); const inputMessages = JSON.parse( - attributes[SpanAttributes.LLM_INPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], ); assert.strictEqual(inputMessages.length, 4); @@ -1370,12 +1370,12 @@ describe("AI SDK Transformations", () => { // Check that gen_ai.output.messages is properly set assert.strictEqual( - typeof attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], "string", ); const outputMessages = JSON.parse( - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], ); assert.strictEqual(outputMessages.length, 1); assert.strictEqual(outputMessages[0].role, "assistant"); @@ -1411,12 +1411,12 @@ describe("AI SDK Transformations", () => { // Check that gen_ai.output.messages is properly set assert.strictEqual( - typeof attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], "string", ); const outputMessages = JSON.parse( - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], ); assert.strictEqual(outputMessages.length, 1); assert.strictEqual(outputMessages[0].role, "assistant"); @@ -1510,11 +1510,11 @@ describe("AI SDK Transformations", () => { // Check input messages assert.strictEqual( - typeof attributes[SpanAttributes.LLM_INPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], "string", ); const parsedInputMessages = JSON.parse( - attributes[SpanAttributes.LLM_INPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], ); assert.strictEqual(parsedInputMessages.length, 2); assert.strictEqual(parsedInputMessages[0].role, "system"); @@ -1530,11 +1530,11 @@ describe("AI SDK Transformations", () => { // Check output messages (tool calls) assert.strictEqual( - typeof attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], "string", ); const parsedOutputMessages = JSON.parse( - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], ); assert.strictEqual(parsedOutputMessages.length, 1); assert.strictEqual(parsedOutputMessages[0].role, "assistant"); @@ -1554,11 +1554,11 @@ describe("AI SDK Transformations", () => { // Check that tools are also properly transformed assert.strictEqual( - attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name`], + attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name`], "getWeather", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.1.name`], + attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.1.name`], "searchRestaurants", ); }); @@ -1579,12 +1579,12 @@ describe("AI SDK Transformations", () => { // Check that gen_ai.output.messages is properly set assert.strictEqual( - typeof attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + typeof attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], "string", ); const outputMessages = JSON.parse( - attributes[SpanAttributes.LLM_OUTPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES], ); assert.strictEqual(outputMessages.length, 1); assert.strictEqual(outputMessages[0].role, "assistant"); @@ -1628,7 +1628,7 @@ describe("AI SDK Transformations", () => { // Check input messages transformation const inputMessages = JSON.parse( - attributes[SpanAttributes.LLM_INPUT_MESSAGES], + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES], ); assert.strictEqual(inputMessages.length, 4); @@ -1705,7 +1705,7 @@ describe("AI SDK Transformations", () => { // Check that other transformations still work assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "Hello!", ); assert.strictEqual(attributes.someOtherAttr, "value"); @@ -1832,15 +1832,15 @@ describe("AI SDK Transformations", () => { // Check other transformations still work assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], "I'll help you with that!", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Help me", ); assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); // Check original attributes are removed assert.strictEqual(attributes["ai.telemetry.metadata.userId"], undefined); @@ -1867,7 +1867,7 @@ describe("AI SDK Transformations", () => { // Check that agent attributes are set assert.strictEqual( - attributes[SpanAttributes.GEN_AI_AGENT_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "research_assistant", ); assert.strictEqual( @@ -1905,7 +1905,7 @@ describe("AI SDK Transformations", () => { // Agent name should be set for context assert.strictEqual( - attributes[SpanAttributes.GEN_AI_AGENT_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "research_assistant", ); @@ -1931,7 +1931,7 @@ describe("AI SDK Transformations", () => { // Agent attributes should not be set assert.strictEqual( - attributes[SpanAttributes.GEN_AI_AGENT_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], undefined, ); assert.strictEqual( @@ -1992,7 +1992,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "ai.generateText"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "chat", ); }); @@ -2005,7 +2005,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "ai.streamText"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "chat", ); }); @@ -2018,7 +2018,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "ai.generateObject"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "chat", ); }); @@ -2031,7 +2031,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "ai.streamObject"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "chat", ); }); @@ -2044,7 +2044,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "ai.toolCall"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "execute_tool", ); }); @@ -2057,7 +2057,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "calculate.tool"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "execute_tool", ); }); @@ -2068,7 +2068,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes, "unknown.span"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], undefined, ); }); @@ -2079,7 +2079,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], undefined, ); }); @@ -2094,10 +2094,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "openai", ); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); }); it("should extract provider name from complex provider string", () => { @@ -2108,10 +2108,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "azure-openai", ); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Azure"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); }); it("should handle simple provider name without dots", () => { @@ -2122,10 +2122,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "anthropic", ); - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Anthropic"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Anthropic"); }); it("should not set provider name when ai.model.provider is not present", () => { @@ -2134,7 +2134,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], undefined, ); }); @@ -2149,7 +2149,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o", ); assert.strictEqual(attributes["ai.model.id"], undefined); @@ -2163,7 +2163,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], undefined, ); assert.strictEqual(attributes.someOtherAttr, "value"); @@ -2179,7 +2179,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.deepStrictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS], ["stop"], ); assert.strictEqual(attributes["ai.response.finishReason"], undefined); @@ -2193,7 +2193,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.deepStrictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS], ["stop", "length"], ); assert.strictEqual(attributes["ai.response.finishReason"], undefined); @@ -2210,7 +2210,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.deepStrictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS], [reason], ); }); @@ -2224,7 +2224,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS], undefined, ); }); @@ -2243,19 +2243,19 @@ describe("AI SDK Transformations", () => { // Check OpenTelemetry standard attributes assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_NAME], "getWeather", ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ID], "call_abc123", ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ARGUMENTS], '{"location":"San Francisco"}', ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_CALL_RESULT], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_RESULT], '{"temperature":72}', ); @@ -2281,15 +2281,15 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_NAME], "calculate", ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ID], undefined, ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ARGUMENTS], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ARGUMENTS], '{"a":5,"b":3}', ); }); @@ -2302,11 +2302,11 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_NAME], undefined, ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_TOOL_CALL_ID], + attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ID], undefined, ); assert.strictEqual(attributes.someOtherAttr, "value"); @@ -2322,7 +2322,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID], "conv_123", ); }); @@ -2335,7 +2335,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID], "session_456", ); }); @@ -2349,7 +2349,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID], "conv_123", ); }); @@ -2362,7 +2362,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID], undefined, ); }); @@ -2377,7 +2377,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.LLM_RESPONSE_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], "gpt-4o-2024-05-13", ); assert.strictEqual(attributes["ai.response.model"], undefined); @@ -2391,7 +2391,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID], "chatcmpl-abc123", ); assert.strictEqual(attributes["ai.response.id"], undefined); @@ -2410,7 +2410,7 @@ describe("AI SDK Transformations", () => { "gpt-4o", ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID], "chatcmpl-xyz789", ); assert.strictEqual(attributes["ai.response.model"], undefined); @@ -2429,7 +2429,7 @@ describe("AI SDK Transformations", () => { undefined, ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID], undefined, ); }); @@ -2457,40 +2457,40 @@ describe("AI SDK Transformations", () => { // Check operation name assert.strictEqual( - attributes[SpanAttributes.GEN_AI_OPERATION_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_OPERATION_NAME], "chat", ); // Check model transformations assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o", ); assert.strictEqual( - attributes[SpanAttributes.LLM_RESPONSE_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], "gpt-4o-2024-05-13", ); // Check provider transformations - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "OpenAI"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "openai", ); // Check response transformations assert.deepStrictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_FINISH_REASONS], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS], ["stop"], ); assert.strictEqual( - attributes[SpanAttributes.GEN_AI_RESPONSE_ID], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID], "chatcmpl-abc123", ); // Check conversation ID assert.strictEqual( - attributes[SpanAttributes.GEN_AI_CONVERSATION_ID], + attributes[SpanAttributes.ATTR_GEN_AI_CONVERSATION_ID], "conv_456", ); From 671117b27b88f2aaa8f7444108dcbb4f1d882dea Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:27:55 +0200 Subject: [PATCH 07/24] test --- .../test/ai-sdk-transformations.test.ts | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 6231f12b..fca05e24 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -9,33 +9,6 @@ import { transformAiSdkSpanNames, } from "../src/lib/tracing/ai-sdk-transformations"; -// Helper function to create a mock ReadableSpan -const createMockSpan = ( - name: string, - attributes: Record = {}, -): ReadableSpan => { - return { - name, - attributes, - instrumentationScope: { name: "ai", version: "1.0.0" }, - } as ReadableSpan; -}; - -// Helper function to create a mock span with updateName capability -const createMockSpanWithUpdate = ( - name: string, - attributes: Record = {}, -) => { - const span = { - name, - attributes, - instrumentationScope: { name: "ai", version: "1.0.0" }, - updateName: (newName: string) => { - span.name = newName; - }, - }; - return span as ReadableSpan & { updateName: (name: string) => void }; -}; describe("AI SDK Transformations", () => { describe("transformAiSdkAttributes - response text", () => { @@ -991,7 +964,7 @@ describe("AI SDK Transformations", () => { it("should not calculate total when input tokens are missing", () => { const attributes = { - [SpanAttributes.LLM_USAGE_OUTPUT_TOKENS]: 25, + [SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 25, }; transformLLMSpans(attributes); @@ -1223,7 +1196,7 @@ describe("AI SDK Transformations", () => { // Check token transformations - should keep input/output tokens assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 5); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_TOTAL_TOKENS], 15); + assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); // Check vendor transformation assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); From 8528f2438fafa3f9db5c69d52b9cff2ab57d0f96 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:31:05 +0200 Subject: [PATCH 08/24] less comments --- .../src/lib/tracing/ai-sdk-transformations.ts | 17 ----------------- 1 file changed, 17 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 b6deb23f..5ac1a9b9 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -310,8 +310,6 @@ const transformPrompts = (attributes: Record): void => { if (AI_PROMPT in attributes) { try { const promptData = JSON.parse(attributes[AI_PROMPT] as string); - - // Handle case where promptData has a "messages" array if (promptData.messages && Array.isArray(promptData.messages)) { const messages = promptData.messages; const inputMessages: any[] = []; @@ -324,7 +322,6 @@ const transformPrompts = (attributes: Record): void => { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; - // Add to OpenTelemetry standard gen_ai.input.messages format inputMessages.push({ role: msg.role, parts: [ @@ -337,7 +334,6 @@ const transformPrompts = (attributes: Record): void => { }, ); - // Set the OpenTelemetry standard input messages attribute if (inputMessages.length > 0) { attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify(inputMessages); @@ -345,7 +341,6 @@ const transformPrompts = (attributes: Record): void => { delete attributes[AI_PROMPT]; } - // Handle case where promptData has a "prompt" string else if (promptData.prompt && typeof promptData.prompt === "string") { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = promptData.prompt; @@ -373,7 +368,6 @@ const transformPrompts = (attributes: Record): void => { }; const transformPromptTokens = (attributes: Record): void => { - // Make sure we have the right naming convention if ( !(SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS in attributes) && AI_USAGE_PROMPT_TOKENS in attributes @@ -382,13 +376,11 @@ const transformPromptTokens = (attributes: Record): void => { attributes[AI_USAGE_PROMPT_TOKENS]; } - // Clean up legacy attributes delete attributes[AI_USAGE_PROMPT_TOKENS]; delete attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]; }; const transformCompletionTokens = (attributes: Record): void => { - // Make sure we have the right naming convention if ( !(SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS in attributes) && AI_USAGE_COMPLETION_TOKENS in attributes @@ -397,7 +389,6 @@ const transformCompletionTokens = (attributes: Record): void => { attributes[AI_USAGE_COMPLETION_TOKENS]; } - // Clean up legacy attributes delete attributes[AI_USAGE_COMPLETION_TOKENS]; delete attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]; }; @@ -465,10 +456,8 @@ const transformVendor = (attributes: Record): void => { if (AI_MODEL_PROVIDER in attributes) { const vendor = attributes[AI_MODEL_PROVIDER]; - // Find matching vendor prefix in mapping let mappedVendor = null; if (typeof vendor === "string" && vendor.length > 0) { - // Extract base provider name for OpenTelemetry standard (e.g., "openai" from "openai.chat") const providerName = vendor.split(".")[0]; attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = providerName; @@ -520,7 +509,6 @@ const transformFinishReason = (attributes: Record): void => { const AI_RESPONSE_FINISH_REASON = "ai.response.finishReason"; if (AI_RESPONSE_FINISH_REASON in attributes) { const finishReason = attributes[AI_RESPONSE_FINISH_REASON]; - // Convert to array format for OTel standard attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS] = Array.isArray( finishReason, ) @@ -531,28 +519,24 @@ const transformFinishReason = (attributes: Record): void => { }; const transformToolCallAttributes = (attributes: Record): void => { - // Transform tool name if ("ai.toolCall.name" in attributes) { attributes[SpanAttributes.ATTR_GEN_AI_TOOL_NAME] = attributes["ai.toolCall.name"]; // Keep ai.toolCall.name for now, will be deleted in transformToolCalls } - // Transform tool call ID if ("ai.toolCall.id" in attributes) { attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ID] = attributes["ai.toolCall.id"]; delete attributes["ai.toolCall.id"]; } - // Transform tool arguments (keep both OTel and Traceloop versions) if ("ai.toolCall.args" in attributes) { attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_ARGUMENTS] = attributes["ai.toolCall.args"]; // Don't delete yet - transformToolCalls will handle entity input/output } - // Transform tool result (keep both OTel and Traceloop versions) if ("ai.toolCall.result" in attributes) { attributes[SpanAttributes.ATTR_GEN_AI_TOOL_CALL_RESULT] = attributes["ai.toolCall.result"]; @@ -561,7 +545,6 @@ const transformToolCallAttributes = (attributes: Record): void => { }; const transformConversationId = (attributes: Record): void => { - // Check for conversation/session ID in metadata const conversationId = attributes["ai.telemetry.metadata.conversationId"]; const sessionId = attributes["ai.telemetry.metadata.sessionId"]; From ff1d6a8b075b6876f90c892d4d07d208fcd607bb Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:40:17 +0200 Subject: [PATCH 09/24] change test --- .../src/instrumentation.ts | 42 +++---- .../src/instrumentation.ts | 112 +++++++++--------- .../test/instrumentation.test.ts | 86 +++++++------- .../src/lib/tracing/ai-sdk-transformations.ts | 46 +++---- .../test/ai-sdk-transformations.test.ts | 107 +++++++++++++---- 5 files changed, 230 insertions(+), 163 deletions(-) diff --git a/packages/instrumentation-anthropic/src/instrumentation.ts b/packages/instrumentation-anthropic/src/instrumentation.ts index 146106c4..01fa2302 100644 --- a/packages/instrumentation-anthropic/src/instrumentation.ts +++ b/packages/instrumentation-anthropic/src/instrumentation.ts @@ -204,14 +204,14 @@ export class AnthropicInstrumentation extends InstrumentationBase { }; }): Span { const attributes: Attributes = { - [SpanAttributes.LLM_SYSTEM]: "Anthropic", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "Anthropic", [SpanAttributes.LLM_REQUEST_TYPE]: type, }; try { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = params.model; - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = params.temperature; - attributes[SpanAttributes.LLM_REQUEST_TOP_P] = params.top_p; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p; attributes[SpanAttributes.LLM_TOP_K] = params.top_k; // Handle thinking parameters (for beta messages) @@ -223,10 +223,10 @@ export class AnthropicInstrumentation extends InstrumentationBase { } if (type === "completion") { - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens_to_sample; } else { - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = params.max_tokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; } if ( @@ -244,8 +244,8 @@ export class AnthropicInstrumentation extends InstrumentationBase { // If a system prompt is provided, it should always be first if ("system" in params && params.system !== undefined) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "system"; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "system"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = typeof params.system === "string" ? params.system : JSON.stringify(params.system); @@ -254,21 +254,21 @@ export class AnthropicInstrumentation extends InstrumentationBase { params.messages.forEach((message, index) => { const currentIndex = index + promptIndex; - attributes[`${SpanAttributes.LLM_PROMPTS}.${currentIndex}.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${currentIndex}.role`] = message.role; if (typeof message.content === "string") { attributes[ - `${SpanAttributes.LLM_PROMPTS}.${currentIndex}.content` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${currentIndex}.content` ] = (message.content as string) || ""; } else { attributes[ - `${SpanAttributes.LLM_PROMPTS}.${currentIndex}.content` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${currentIndex}.content` ] = JSON.stringify(message.content); } }); } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; } } } catch (e) { @@ -477,25 +477,25 @@ export class AnthropicInstrumentation extends InstrumentationBase { result: Completion; }) { try { - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, result.model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result.model); if (type === "chat" && result.usage) { span.setAttribute( SpanAttributes.LLM_USAGE_TOTAL_TOKENS, result.usage?.input_tokens + result.usage?.output_tokens, ); span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result.usage?.output_tokens, ); span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result.usage?.input_tokens, ); } if (result.stop_reason) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`, result.stop_reason, ); } @@ -503,20 +503,20 @@ export class AnthropicInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { if (type === "chat") { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, JSON.stringify(result.content), ); } else { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, result.completion, ); } diff --git a/packages/instrumentation-bedrock/src/instrumentation.ts b/packages/instrumentation-bedrock/src/instrumentation.ts index 4afbc097..ac44a6f8 100644 --- a/packages/instrumentation-bedrock/src/instrumentation.ts +++ b/packages/instrumentation-bedrock/src/instrumentation.ts @@ -198,13 +198,13 @@ export class BedrockInstrumentation extends InstrumentationBase { ? (span["attributes"] as Record) : {}; - if (SpanAttributes.LLM_SYSTEM in attributes) { + if (SpanAttributes.ATTR_GEN_AI_SYSTEM in attributes) { const modelId = attributes[ - SpanAttributes.LLM_RESPONSE_MODEL + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL ] as string; const { modelVendor, model } = this._extractVendorAndModel(modelId); - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, model); if (!(result.body instanceof Object.getPrototypeOf(Uint8Array))) { const rawRes = result.body as AsyncIterable; @@ -218,13 +218,13 @@ export class BedrockInstrumentation extends InstrumentationBase { if ("amazon-bedrock-invocationMetrics" in parsedResponse) { span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, parsedResponse["amazon-bedrock-invocationMetrics"][ "inputTokenCount" ], ); span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, parsedResponse["amazon-bedrock-invocationMetrics"][ "outputTokenCount" ], @@ -252,12 +252,12 @@ export class BedrockInstrumentation extends InstrumentationBase { // Update local value with attribute value that was set by _setResponseAttributes streamedContent += responseAttributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ]; // re-assign the new value to responseAttributes responseAttributes = { ...responseAttributes, - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: streamedContent, }; } @@ -297,9 +297,9 @@ export class BedrockInstrumentation extends InstrumentationBase { switch (vendor) { case "ai21": { return { - [SpanAttributes.LLM_REQUEST_TOP_P]: requestBody["topP"], - [SpanAttributes.LLM_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.LLM_REQUEST_MAX_TOKENS]: requestBody["maxTokens"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["topP"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["maxTokens"], [SpanAttributes.LLM_PRESENCE_PENALTY]: requestBody["presencePenalty"]["scale"], [SpanAttributes.LLM_FREQUENCY_PENALTY]: @@ -308,8 +308,8 @@ export class BedrockInstrumentation extends InstrumentationBase { // Prompt & Role ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user", - [`${SpanAttributes.LLM_PROMPTS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`]: "user", + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody["prompt"], } : {}), @@ -317,18 +317,18 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "amazon": { return { - [SpanAttributes.LLM_REQUEST_TOP_P]: + [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["textGenerationConfig"]["topP"], - [SpanAttributes.LLM_REQUEST_TEMPERATURE]: + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["textGenerationConfig"]["temperature"], - [SpanAttributes.LLM_REQUEST_MAX_TOKENS]: + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["textGenerationConfig"]["maxTokenCount"], // Prompt & Role ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user", - [`${SpanAttributes.LLM_PROMPTS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`]: "user", + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody["inputText"], } : {}), @@ -336,10 +336,10 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "anthropic": { const baseAttributes = { - [SpanAttributes.LLM_REQUEST_TOP_P]: requestBody["top_p"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["top_p"], [SpanAttributes.LLM_TOP_K]: requestBody["top_k"], - [SpanAttributes.LLM_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.LLM_REQUEST_MAX_TOKENS]: + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["max_tokens_to_sample"] || requestBody["max_tokens"], }; @@ -351,9 +351,9 @@ export class BedrockInstrumentation extends InstrumentationBase { if (requestBody["messages"]) { const promptAttributes: Record = {}; requestBody["messages"].forEach((message: any, index: number) => { - promptAttributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = + promptAttributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = message.role; - promptAttributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] = + promptAttributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = typeof message.content === "string" ? message.content : JSON.stringify(message.content); @@ -365,8 +365,8 @@ export class BedrockInstrumentation extends InstrumentationBase { if (requestBody["prompt"]) { return { ...baseAttributes, - [`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user", - [`${SpanAttributes.LLM_PROMPTS}.0.content`]: requestBody["prompt"] + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`]: "user", + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody["prompt"] // The format is removing when we are setting span attribute .replace("\n\nHuman:", "") .replace("\n\nAssistant:", ""), @@ -377,16 +377,16 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "cohere": { return { - [SpanAttributes.LLM_REQUEST_TOP_P]: requestBody["p"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["p"], [SpanAttributes.LLM_TOP_K]: requestBody["k"], - [SpanAttributes.LLM_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.LLM_REQUEST_MAX_TOKENS]: requestBody["max_tokens"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["max_tokens"], // Prompt & Role ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user", - [`${SpanAttributes.LLM_PROMPTS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`]: "user", + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody["message"] || requestBody["prompt"], } : {}), @@ -394,15 +394,15 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "meta": { return { - [SpanAttributes.LLM_REQUEST_TOP_P]: requestBody["top_p"], - [SpanAttributes.LLM_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.LLM_REQUEST_MAX_TOKENS]: requestBody["max_gen_len"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["top_p"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["max_gen_len"], // Prompt & Role ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user", - [`${SpanAttributes.LLM_PROMPTS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`]: "user", + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody["prompt"], } : {}), @@ -421,12 +421,12 @@ export class BedrockInstrumentation extends InstrumentationBase { switch (vendor) { case "ai21": { return { - [`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`]: response["completions"][0]["finishReason"]["reason"], - [`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant", + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`]: "assistant", ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: response["completions"][0]["data"]["text"], } : {}), @@ -434,13 +434,13 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "amazon": { return { - [`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]: isStream + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`]: isStream ? response["completionReason"] : response["results"][0]["completionReason"], - [`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant", - [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`]: "assistant", + [SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]: response["inputTextTokenCount"], - [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: isStream + [SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]: isStream ? response["totalOutputTextTokenCount"] : response["results"][0]["tokenCount"], [SpanAttributes.LLM_USAGE_TOTAL_TOKENS]: isStream @@ -450,7 +450,7 @@ export class BedrockInstrumentation extends InstrumentationBase { response["results"][0]["tokenCount"], ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: isStream + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: isStream ? response["outputText"] : response["results"][0]["outputText"], } @@ -459,9 +459,9 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "anthropic": { const baseAttributes = { - [`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`]: response["stop_reason"], - [`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant", + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`]: "assistant", }; if (!this._shouldSendPrompts()) { @@ -475,7 +475,7 @@ export class BedrockInstrumentation extends InstrumentationBase { : response["content"]; return { ...baseAttributes, - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: content, + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: content, }; } @@ -483,7 +483,7 @@ export class BedrockInstrumentation extends InstrumentationBase { if (response["completion"]) { return { ...baseAttributes, - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: response["completion"], }; } @@ -492,12 +492,12 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "cohere": { const baseAttributes = { - [`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`]: response["generations"]?.[0]?.["finish_reason"], - [`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant", + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`]: "assistant", ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: response["generations"]?.[0]?.["text"], } : {}), @@ -508,9 +508,9 @@ export class BedrockInstrumentation extends InstrumentationBase { const billedUnits = response["meta"]["billed_units"]; return { ...baseAttributes, - [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: + [SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]: billedUnits["input_tokens"], - [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: + [SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]: billedUnits["output_tokens"], [SpanAttributes.LLM_USAGE_TOTAL_TOKENS]: (billedUnits["input_tokens"] || 0) + @@ -522,18 +522,18 @@ export class BedrockInstrumentation extends InstrumentationBase { } case "meta": { return { - [`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`]: response["stop_reason"], - [`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant", - [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`]: "assistant", + [SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]: response["prompt_token_count"], - [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: + [SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]: response["generation_token_count"], [SpanAttributes.LLM_USAGE_TOTAL_TOKENS]: response["prompt_token_count"] + response["generation_token_count"], ...(this._shouldSendPrompts() ? { - [`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`]: response["generation"], } : {}), diff --git a/packages/instrumentation-together/test/instrumentation.test.ts b/packages/instrumentation-together/test/instrumentation.test.ts index d88892c7..3324c435 100644 --- a/packages/instrumentation-together/test/instrumentation.test.ts +++ b/packages/instrumentation-together/test/instrumentation.test.ts @@ -123,28 +123,28 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What's the weather like in Boston?", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name` + `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name` ], "get_current_weather", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.description` + `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.description` ], "Get the current weather in a given location", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.arguments` ], JSON.stringify({ type: "object", @@ -160,14 +160,14 @@ describe("Test Together instrumentation", async function () { ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.function_call.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` ], "get_current_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.function_call.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` ]! as string, ), { location: "Boston, MA" }, @@ -176,12 +176,12 @@ describe("Test Together instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], 333, ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -204,23 +204,23 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); assert.ok( completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], 37, ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); } catch (error) { @@ -254,16 +254,16 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ], result, ); @@ -271,12 +271,12 @@ describe("Test Together instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], 37, ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); } catch (error) { @@ -300,11 +300,11 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); assert.ok( @@ -312,20 +312,20 @@ describe("Test Together instrumentation", async function () { ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.ok( typeof completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ] === "string" && ( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ] as string ).length > 0, ); @@ -356,12 +356,12 @@ describe("Test Together instrumentation", async function () { assert.ok(result, "Result should not be empty"); assert.ok(completionSpan, "Completion span should exist"); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", "Prompt role should be 'user'", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", "Prompt content should match input", ); @@ -371,22 +371,22 @@ describe("Test Together instrumentation", async function () { ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, "Completion tokens should be greater than 0", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", "Completion role should be 'assistant'", ); assert.ok( typeof completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ] === "string" && ( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ] as string ).length > 0, "Completion content should not be empty", @@ -430,28 +430,28 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What's the weather like in Boston?", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name` + `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name` ], "get_current_weather", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.description` + `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.description` ], "Get the current weather in a given location", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.arguments` ], JSON.stringify({ type: "object", @@ -467,14 +467,14 @@ describe("Test Together instrumentation", async function () { ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` ], "get_current_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` ]! as string, ), { location: "Boston, MA" }, @@ -483,12 +483,12 @@ describe("Test Together instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], 333, ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -562,28 +562,28 @@ describe("Test Together instrumentation", async function () { assert.ok(completionSpan); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` ], "get_current_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` ]! as string, ), { location: "Boston, MA", unit: "fahrenheit" }, ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.1.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` ], "get_tomorrow_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.1.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` ]! as string, ), { location: "Chicago, IL", unit: "fahrenheit" }, 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 5ac1a9b9..bbf8a0e5 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -75,7 +75,8 @@ const transformResponseText = (attributes: Record): void => { if (AI_RESPONSE_TEXT in attributes) { attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = attributes[AI_RESPONSE_TEXT]; - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = ROLE_ASSISTANT; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + ROLE_ASSISTANT; const outputMessage = { role: ROLE_ASSISTANT, @@ -98,7 +99,8 @@ const transformResponseObject = (attributes: Record): void => { if (AI_RESPONSE_OBJECT in attributes) { attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = attributes[AI_RESPONSE_OBJECT]; - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = ROLE_ASSISTANT; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + ROLE_ASSISTANT; const outputMessage = { role: ROLE_ASSISTANT, @@ -124,7 +126,8 @@ const transformResponseToolCalls = (attributes: Record): void => { attributes[AI_RESPONSE_TOOL_CALLS] as string, ); - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = ROLE_ASSISTANT; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + ROLE_ASSISTANT; const toolCallParts: any[] = []; toolCalls.forEach((toolCall: any, index: number) => { @@ -151,9 +154,9 @@ const transformResponseToolCalls = (attributes: Record): void => { role: ROLE_ASSISTANT, parts: toolCallParts, }; - attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES] = JSON.stringify([ - outputMessage, - ]); + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES] = JSON.stringify( + [outputMessage], + ); } delete attributes[AI_RESPONSE_TOOL_CALLS]; @@ -281,7 +284,8 @@ const transformPrompts = (attributes: Record): void => { const processedContent = processMessageContent(msg.content); const contentKey = `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`; attributes[contentKey] = processedContent; - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = + msg.role; // Add to OpenTelemetry standard gen_ai.input.messages format inputMessages.push({ @@ -340,8 +344,7 @@ const transformPrompts = (attributes: Record): void => { } delete attributes[AI_PROMPT]; - } - else if (promptData.prompt && typeof promptData.prompt === "string") { + } else if (promptData.prompt && typeof promptData.prompt === "string") { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = promptData.prompt; attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = ROLE_USER; @@ -444,7 +447,8 @@ const transformProviderMetadata = (attributes: Record): void => { const calculateTotalTokens = (attributes: Record): void => { const inputTokens = attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]; - const outputTokens = attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]; + const outputTokens = + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]; if (inputTokens && outputTokens) { attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`] = @@ -469,7 +473,8 @@ const transformVendor = (attributes: Record): void => { } } - attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = mappedVendor || vendor; + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = + mappedVendor || vendor; delete attributes[AI_MODEL_PROVIDER]; } }; @@ -500,7 +505,8 @@ const transformOperationName = ( const transformModelId = (attributes: Record): void => { const AI_MODEL_ID = "ai.model.id"; if (AI_MODEL_ID in attributes) { - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = attributes[AI_MODEL_ID]; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = + attributes[AI_MODEL_ID]; delete attributes[AI_MODEL_ID]; } }; @@ -509,11 +515,8 @@ const transformFinishReason = (attributes: Record): void => { const AI_RESPONSE_FINISH_REASON = "ai.response.finishReason"; if (AI_RESPONSE_FINISH_REASON in attributes) { const finishReason = attributes[AI_RESPONSE_FINISH_REASON]; - attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS] = Array.isArray( - finishReason, - ) - ? finishReason - : [finishReason]; + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_FINISH_REASONS] = + Array.isArray(finishReason) ? finishReason : [finishReason]; delete attributes[AI_RESPONSE_FINISH_REASON]; } }; @@ -566,7 +569,8 @@ const transformResponseMetadata = (attributes: Record): void => { } if (AI_RESPONSE_ID in attributes) { - attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID] = attributes[AI_RESPONSE_ID]; + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_ID] = + attributes[AI_RESPONSE_ID]; delete attributes[AI_RESPONSE_ID]; } }; @@ -619,8 +623,10 @@ const transformTelemetryMetadata = ( TraceloopSpanKindValues.AGENT; attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] = agentName; - const inputMessages = attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES]; - const outputMessages = attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES]; + const inputMessages = + attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES]; + const outputMessages = + attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES]; const toolArgs = attributes["ai.toolCall.args"]; const toolResult = attributes["ai.toolCall.result"]; diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index fca05e24..1ed0d42d 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -9,7 +9,6 @@ import { transformAiSdkSpanNames, } from "../src/lib/tracing/ai-sdk-transformations"; - describe("AI SDK Transformations", () => { describe("transformAiSdkAttributes - response text", () => { it("should transform ai.response.text to completion attributes", () => { @@ -127,7 +126,9 @@ describe("AI SDK Transformations", () => { // Check first tool call assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.name`], + attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.name` + ], "getWeather", ); assert.strictEqual( @@ -139,7 +140,9 @@ describe("AI SDK Transformations", () => { // Check second tool call assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.name`], + attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.name` + ], "searchRestaurants", ); assert.strictEqual( @@ -405,7 +408,8 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - const result = attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]; + const result = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]; // The escape sequences should be properly unescaped assert.strictEqual( @@ -857,7 +861,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 50); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], + 50, + ); assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], @@ -886,7 +893,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 0); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], + 0, + ); assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); }); }); @@ -934,7 +944,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 0); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], + 0, + ); assert.strictEqual(attributes["ai.usage.completionTokens"], undefined); }); }); @@ -1009,7 +1022,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "OpenAI", + ); assert.strictEqual(attributes["ai.model.provider"], undefined); assert.strictEqual(attributes.someOtherAttr, "value"); }); @@ -1028,7 +1044,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "OpenAI", + ); assert.strictEqual(attributes["ai.model.provider"], undefined); }); }); @@ -1043,7 +1062,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "Azure", + ); assert.strictEqual(attributes["ai.model.provider"], undefined); }); }); @@ -1055,7 +1077,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Anthropic"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "Anthropic", + ); assert.strictEqual(attributes["ai.model.provider"], undefined); }); @@ -1077,7 +1102,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], ""); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "", + ); assert.strictEqual(attributes["ai.model.provider"], undefined); }); }); @@ -1118,12 +1146,21 @@ describe("AI SDK Transformations", () => { ); // Check token transformations - should keep input/output tokens - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 5); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], + 10, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], + 5, + ); assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); // Check vendor transformation - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "OpenAI", + ); // Check original AI SDK attributes are removed assert.strictEqual(attributes["ai.response.text"], undefined); @@ -1194,12 +1231,21 @@ describe("AI SDK Transformations", () => { ); // Check token transformations - should keep input/output tokens - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 5); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS], + 10, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], + 5, + ); assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); // Check vendor transformation - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "Azure", + ); // Check original AI SDK attributes are removed assert.strictEqual(attributes["ai.response.object"], undefined); @@ -1813,7 +1859,10 @@ describe("AI SDK Transformations", () => { "Help me", ); assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "OpenAI", + ); // Check original attributes are removed assert.strictEqual(attributes["ai.telemetry.metadata.userId"], undefined); @@ -2070,7 +2119,10 @@ describe("AI SDK Transformations", () => { attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "openai", ); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "OpenAI", + ); }); it("should extract provider name from complex provider string", () => { @@ -2084,7 +2136,10 @@ describe("AI SDK Transformations", () => { attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "azure-openai", ); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Azure"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "Azure", + ); }); it("should handle simple provider name without dots", () => { @@ -2098,7 +2153,10 @@ describe("AI SDK Transformations", () => { attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "anthropic", ); - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "Anthropic"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "Anthropic", + ); }); it("should not set provider name when ai.model.provider is not present", () => { @@ -2445,7 +2503,10 @@ describe("AI SDK Transformations", () => { ); // Check provider transformations - assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "OpenAI"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "OpenAI", + ); assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], "openai", From ac489cbb2d89ea03e9ff4e8c0af26fa04fd93b36 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:45:23 +0200 Subject: [PATCH 10/24] change to att --- .../test/instrumentation.test.ts | 82 ++++++---- .../src/instrumentation.ts | 6 +- .../tests/ai21.test.ts | 28 ++-- .../tests/amazon.test.ts | 73 +++++---- .../tests/anthropic.test.ts | 59 ++++--- .../tests/cohere.test.ts | 68 +++++--- .../tests/meta.test.ts | 73 +++++---- .../src/instrumentation.ts | 64 ++++---- .../instrumentation-cohere/tests/chat.test.ts | 56 ++++--- .../tests/generate.test.ts | 50 +++--- .../tests/rerank.test.ts | 14 +- .../src/callback_handler.ts | 31 ++-- .../test/instrumentation.test.ts | 32 ++-- .../src/custom-llm-instrumentation.ts | 24 +-- .../src/image-wrappers.ts | 48 +++--- .../src/instrumentation.ts | 48 +++--- .../test/instrumentation.test.ts | 154 +++++++++++------- .../src/instrumentation.ts | 52 +++--- .../test/instrumentation.test.ts | 68 +++++--- .../src/aiplatform-instrumentation.ts | 34 ++-- .../src/vertexai-instrumentation.ts | 30 ++-- .../tests/gemini.test.ts | 39 ++++- .../tests/palm2.test.ts | 34 +++- .../src/lib/tracing/ai-sdk-transformations.ts | 16 +- .../src/lib/tracing/decorators.ts | 2 +- .../traceloop-sdk/src/lib/tracing/manual.ts | 26 +-- .../src/lib/tracing/span-processor.ts | 9 +- .../test/agent_decorator.test.ts | 22 +-- .../test/ai-sdk-agent-integration.test.ts | 12 +- .../test/ai-sdk-integration.test.ts | 41 +++-- .../test/ai-sdk-transformations.test.ts | 73 ++++++--- .../traceloop-sdk/test/decorators.test.ts | 72 +++++--- 32 files changed, 861 insertions(+), 579 deletions(-) diff --git a/packages/instrumentation-anthropic/test/instrumentation.test.ts b/packages/instrumentation-anthropic/test/instrumentation.test.ts index 4b1a0db4..217e9728 100644 --- a/packages/instrumentation-anthropic/test/instrumentation.test.ts +++ b/packages/instrumentation-anthropic/test/instrumentation.test.ts @@ -106,44 +106,49 @@ describe("Test Anthropic instrumentation", async function () { assert.ok(message); assert.ok(chatSpan); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL}`], "claude-3-opus-20240229", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL}`], "claude-3-opus-20240229", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MAX_TOKENS}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS}`], 1024, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], `Tell me a joke about OpenTelemetry`, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], JSON.stringify(message.content), ); assert.equal( - chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], 17, ); assert.ok( - +chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]! > - 0, + +chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` + ]! > 0, ); assert.equal( - +chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`]! + - +chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]!, + +chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ]! + + +chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` + ]!, chatSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); }).timeout(30000); @@ -164,44 +169,49 @@ describe("Test Anthropic instrumentation", async function () { assert.ok(message); assert.ok(chatSpan); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL}`], "claude-3-opus-20240229", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL}`], "claude-3-opus-20240229", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MAX_TOKENS}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS}`], 1024, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], `Tell me a joke about OpenTelemetry`, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], JSON.stringify(message.content), ); assert.equal( - chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], 17, ); assert.ok( - +chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]! > - 0, + +chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` + ]! > 0, ); assert.equal( - +chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`]! + - +chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]!, + +chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ]! + + +chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` + ]!, chatSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); }).timeout(30000); @@ -222,15 +232,15 @@ describe("Test Anthropic instrumentation", async function () { assert.ok(span); assert.strictEqual( - span.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "system", ); assert.strictEqual( - span.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "You are a helpful assistant", ); assert.strictEqual( - span.attributes[`${SpanAttributes.LLM_PROMPTS}.1.role`], + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.role`], "user", ); }).timeout(30000); @@ -258,15 +268,15 @@ describe("Test Anthropic instrumentation", async function () { assert.ok(message); assert.ok(chatSpan); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL}`], "claude-opus-4-1-20250805", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL}`], "claude-opus-4-1-20250805", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MAX_TOKENS}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS}`], 2048, ); @@ -282,18 +292,18 @@ describe("Test Anthropic instrumentation", async function () { // Check prompts assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What is 2+2? Think through this step by step.", ); // Check that we capture both thinking and regular content blocks const content = JSON.parse( chatSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ] as string, ); assert.ok(Array.isArray(content)); @@ -321,9 +331,11 @@ describe("Test Anthropic instrumentation", async function () { // Verify token usage includes thinking tokens const completionTokens = - chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]; + chatSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` + ]; const promptTokens = - chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`]; + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`]; const totalTokens = chatSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`]; diff --git a/packages/instrumentation-bedrock/src/instrumentation.ts b/packages/instrumentation-bedrock/src/instrumentation.ts index ac44a6f8..8466dd69 100644 --- a/packages/instrumentation-bedrock/src/instrumentation.ts +++ b/packages/instrumentation-bedrock/src/instrumentation.ts @@ -157,9 +157,9 @@ export class BedrockInstrumentation extends InstrumentationBase { ); attributes = { - [SpanAttributes.LLM_SYSTEM]: "AWS", - [SpanAttributes.LLM_REQUEST_MODEL]: model, - [SpanAttributes.LLM_RESPONSE_MODEL]: input.modelId, + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "AWS", + [SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL]: model, + [SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL]: input.modelId, [SpanAttributes.LLM_REQUEST_TYPE]: LLMRequestTypeValues.COMPLETION, }; diff --git a/packages/instrumentation-bedrock/tests/ai21.test.ts b/packages/instrumentation-bedrock/tests/ai21.test.ts index 9a005e6a..12f85ac8 100644 --- a/packages/instrumentation-bedrock/tests/ai21.test.ts +++ b/packages/instrumentation-bedrock/tests/ai21.test.ts @@ -134,14 +134,17 @@ describe("Test Ai21 with AWS Bedrock Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.topP, ); assert.strictEqual( @@ -153,32 +156,35 @@ describe("Test Ai21 with AWS Bedrock Instrumentation", () => { params.frequencyPenalty.scale, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.maxTokens, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`], parsedResponse["completions"][0]["finishReason"]["reason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["completions"][0]["data"]["text"], ); }); diff --git a/packages/instrumentation-bedrock/tests/amazon.test.ts b/packages/instrumentation-bedrock/tests/amazon.test.ts index 0e701170..bfb6f013 100644 --- a/packages/instrumentation-bedrock/tests/amazon.test.ts +++ b/packages/instrumentation-bedrock/tests/amazon.test.ts @@ -135,43 +135,49 @@ describe("Test Amazon Titan with AWS Bedrock Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.textGenerationConfig.topP, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.textGenerationConfig.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.textGenerationConfig.maxTokenCount, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["inputTextTokenCount"], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["results"][0]["tokenCount"], ); assert.strictEqual( @@ -180,11 +186,11 @@ describe("Test Amazon Titan with AWS Bedrock Instrumentation", () => { parsedResponse["results"][0]["tokenCount"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`], parsedResponse["results"][0]["completionReason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["results"][0]["outputText"], ); }); @@ -220,43 +226,52 @@ describe("Test Amazon Titan with AWS Bedrock Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "AWS", + ); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.textGenerationConfig.topP, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.textGenerationConfig.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.textGenerationConfig.maxTokenCount, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["inputTextTokenCount"], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["totalOutputTextTokenCount"], ); assert.strictEqual( @@ -265,23 +280,25 @@ describe("Test Amazon Titan with AWS Bedrock Instrumentation", () => { parsedResponse["totalOutputTextTokenCount"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason` + ], parsedResponse["completionReason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["outputText"], ); if ("amazon-bedrock-invocationMetrics" in parsedResponse) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["amazon-bedrock-invocationMetrics"][ "inputTokenCount" ], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["amazon-bedrock-invocationMetrics"][ "outputTokenCount" ], diff --git a/packages/instrumentation-bedrock/tests/anthropic.test.ts b/packages/instrumentation-bedrock/tests/anthropic.test.ts index c7329546..591acc35 100644 --- a/packages/instrumentation-bedrock/tests/anthropic.test.ts +++ b/packages/instrumentation-bedrock/tests/anthropic.test.ts @@ -133,40 +133,46 @@ describe("Test Anthropic with AWS Bedrock Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.top_p, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.top_k); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.max_tokens_to_sample, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["completion"], ); }); @@ -207,50 +213,59 @@ describe("Test Anthropic with AWS Bedrock Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "AWS", + ); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.top_p, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.top_k); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.max_tokens_to_sample, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], content, ); if ("amazon-bedrock-invocationMetrics" in result) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], result["amazon-bedrock-invocationMetrics"]["inputTokenCount"], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], result["amazon-bedrock-invocationMetrics"]["outputTokenCount"], ); assert.strictEqual( diff --git a/packages/instrumentation-bedrock/tests/cohere.test.ts b/packages/instrumentation-bedrock/tests/cohere.test.ts index a86c0c97..2ab371cc 100644 --- a/packages/instrumentation-bedrock/tests/cohere.test.ts +++ b/packages/instrumentation-bedrock/tests/cohere.test.ts @@ -133,41 +133,50 @@ describe("Test Cohere with AWS Bedrock Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TOP_P], params.p); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + params.p, + ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.k); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.max_tokens, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`], parsedResponse["generations"][0]["finish_reason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["generations"][0]["text"], ); }); @@ -201,56 +210,67 @@ describe("Test Cohere with AWS Bedrock Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "AWS", + ); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.p, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.k); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.max_tokens, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason` + ], parsedResponse["generations"][0]["finish_reason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["generations"][0]["text"], ); if ("amazon-bedrock-invocationMetrics" in parsedResponse) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["amazon-bedrock-invocationMetrics"][ "inputTokenCount" ], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["amazon-bedrock-invocationMetrics"][ "outputTokenCount" ], diff --git a/packages/instrumentation-bedrock/tests/meta.test.ts b/packages/instrumentation-bedrock/tests/meta.test.ts index 9c0cf9d4..543092c2 100644 --- a/packages/instrumentation-bedrock/tests/meta.test.ts +++ b/packages/instrumentation-bedrock/tests/meta.test.ts @@ -132,43 +132,49 @@ describe("Test Meta with AWS Bedrock Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.top_p, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.max_gen_len, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["prompt_token_count"], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["generation_token_count"], ); assert.strictEqual( @@ -177,11 +183,11 @@ describe("Test Meta with AWS Bedrock Instrumentation", () => { parsedResponse["generation_token_count"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`], parsedResponse["stop_reason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["generation"], ); }); @@ -214,43 +220,52 @@ describe("Test Meta with AWS Bedrock Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "AWS", + ); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TOP_P], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], params.top_p, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], params.max_gen_len, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], prompt, ); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_MODEL], model); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["prompt_token_count"], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["generation_token_count"], ); assert.strictEqual( @@ -259,23 +274,25 @@ describe("Test Meta with AWS Bedrock Instrumentation", () => { parsedResponse["generation_token_count"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason` + ], parsedResponse["stop_reason"], ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], parsedResponse["generation"], ); if ("amazon-bedrock-invocationMetrics" in parsedResponse) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], parsedResponse["amazon-bedrock-invocationMetrics"][ "inputTokenCount" ], ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], parsedResponse["amazon-bedrock-invocationMetrics"][ "outputTokenCount" ], diff --git a/packages/instrumentation-cohere/src/instrumentation.ts b/packages/instrumentation-cohere/src/instrumentation.ts index 8ad7f2b4..43cc1199 100644 --- a/packages/instrumentation-cohere/src/instrumentation.ts +++ b/packages/instrumentation-cohere/src/instrumentation.ts @@ -216,24 +216,24 @@ export class CohereInstrumentation extends InstrumentationBase { type: LLM_COMPLETION_TYPE; }): Span { const attributes: Attributes = { - [SpanAttributes.LLM_SYSTEM]: "Cohere", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "Cohere", [SpanAttributes.LLM_REQUEST_TYPE]: this._getLlmRequestTypeByMethod(type), }; try { const model = params.model ?? "command"; - attributes[SpanAttributes.LLM_REQUEST_MODEL] = model; - attributes[SpanAttributes.LLM_REQUEST_MODEL] = model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = model; if (!("query" in params)) { - attributes[SpanAttributes.LLM_REQUEST_TOP_P] = params.p; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.p; attributes[SpanAttributes.LLM_TOP_K] = params.k; - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; attributes[SpanAttributes.LLM_FREQUENCY_PENALTY] = params.frequencyPenalty; attributes[SpanAttributes.LLM_PRESENCE_PENALTY] = params.presencePenalty; - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = params.maxTokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.maxTokens; } else { attributes["topN"] = params["topN"]; attributes["maxChunksPerDoc"] = params["maxChunksPerDoc"]; @@ -241,27 +241,27 @@ export class CohereInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { if (type === "completion" && "prompt" in params) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.user`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`] = params.prompt; } else if (type === "chat" && "message" in params) { params.chatHistory?.forEach((msg, index) => { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; if (msg.role !== "TOOL") { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = msg.message; } }); attributes[ - `${SpanAttributes.LLM_PROMPTS}.${params.chatHistory?.length ?? 0}.role` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${params.chatHistory?.length ?? 0}.role` ] = "user"; attributes[ - `${SpanAttributes.LLM_PROMPTS}.${params.chatHistory?.length ?? 0}.user` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${params.chatHistory?.length ?? 0}.user` ] = params.message; } else if (type === "rerank" && "query" in params) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.user`] = params.query; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`] = params.query; params.documents.forEach((doc, index) => { attributes[`documents.${index}.index`] = typeof doc === "string" ? doc : doc.text; @@ -364,13 +364,13 @@ export class CohereInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { result.results.forEach((each, idx) => { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${idx}.relevanceScore`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${idx}.relevanceScore`, each.relevanceScore, ); if (each.document && each.document?.text) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${idx}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${idx}.content`, each.document.text, ); } @@ -378,12 +378,12 @@ export class CohereInstrumentation extends InstrumentationBase { } else { result.results.forEach((each, idx) => { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${idx}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${idx}.content`, each.index, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${idx}.relevanceScore`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${idx}.relevanceScore`, each.relevanceScore, ); }); @@ -407,7 +407,7 @@ export class CohereInstrumentation extends InstrumentationBase { typeof result.token_count.prompt_tokens === "number" ) { span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result.token_count?.prompt_tokens, ); } @@ -418,7 +418,7 @@ export class CohereInstrumentation extends InstrumentationBase { typeof result.token_count.response_tokens === "number" ) { span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result.token_count?.response_tokens, ); } @@ -437,17 +437,17 @@ export class CohereInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, result.text, ); if (result.searchQueries?.[0].text) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.searchQuery`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.searchQuery`, result.searchQueries?.[0].text, ); } @@ -456,12 +456,12 @@ export class CohereInstrumentation extends InstrumentationBase { result.searchResults.forEach((searchResult, index) => { if (searchResult.searchQuery) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.searchResult.${index}.text`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.searchResult.${index}.text`, searchResult.searchQuery.text, ); } span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.searchResult.${index}.connector`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.searchResult.${index}.connector`, searchResult.connector.id, ); }); @@ -470,7 +470,7 @@ export class CohereInstrumentation extends InstrumentationBase { if ("finishReason" in result && typeof result.finishReason === "string") { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`, result.finishReason, ); } @@ -488,14 +488,14 @@ export class CohereInstrumentation extends InstrumentationBase { if (result && "meta" in result) { if (typeof result.meta?.billedUnits?.inputTokens === "number") { span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result.meta?.billedUnits?.inputTokens, ); } if (typeof result.meta?.billedUnits?.outputTokens === "number") { span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result.meta?.billedUnits?.outputTokens, ); } @@ -514,11 +514,11 @@ export class CohereInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts() && result.generations) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, result.generations[0].text, ); } @@ -529,7 +529,7 @@ export class CohereInstrumentation extends InstrumentationBase { typeof result.generations[0].finish_reason === "string" ) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`, result.generations[0].finish_reason, ); } @@ -540,7 +540,7 @@ export class CohereInstrumentation extends InstrumentationBase { typeof result.generations[0].finishReason === "string" ) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`, result.generations[0].finishReason, ); } diff --git a/packages/instrumentation-cohere/tests/chat.test.ts b/packages/instrumentation-cohere/tests/chat.test.ts index a950778b..ac07dd11 100644 --- a/packages/instrumentation-cohere/tests/chat.test.ts +++ b/packages/instrumentation-cohere/tests/chat.test.ts @@ -99,29 +99,32 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Cohere"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Cohere"); assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TYPE], "chat"); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); assert.strictEqual( attributes[ - `${SpanAttributes.LLM_PROMPTS}.${params.chatHistory?.length ?? 0}.role` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${params.chatHistory?.length ?? 0}.role` ], "user", ); assert.strictEqual( attributes[ - `${SpanAttributes.LLM_PROMPTS}.${params.chatHistory?.length ?? 0}.user` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${params.chatHistory?.length ?? 0}.user` ], params.message, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.k); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TOP_P], params.p); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + params.p, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( @@ -133,7 +136,7 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { params.frequencyPenalty, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); @@ -149,11 +152,11 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { typeof response.token_count.total_tokens === "number" ) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], response.token_count.prompt_tokens, ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], response.token_count.response_tokens, ); assert.strictEqual( @@ -162,16 +165,16 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { ); } assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], response.text, ); if ("finishReason" in response && response.finishReason) { assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason`], response.finishReason, ); } @@ -196,29 +199,32 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Cohere"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Cohere"); assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TYPE], "chat"); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); assert.strictEqual( attributes[ - `${SpanAttributes.LLM_PROMPTS}.${params.chatHistory?.length ?? 0}.role` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${params.chatHistory?.length ?? 0}.role` ], "user", ); assert.strictEqual( attributes[ - `${SpanAttributes.LLM_PROMPTS}.${params.chatHistory?.length ?? 0}.user` + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${params.chatHistory?.length ?? 0}.user` ], params.message, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.k); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TOP_P], params.p); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + params.p, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( @@ -230,7 +236,7 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { params.frequencyPenalty, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); @@ -250,11 +256,11 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { typeof response.token_count.total_tokens === "number" ) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], response.token_count.prompt_tokens, ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], response.token_count.response_tokens, ); assert.strictEqual( @@ -263,16 +269,18 @@ describe.skip("Test Chat with Cohere Instrumentation", () => { ); } assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], response.text, ); if ("finishReason" in response && response.finishReason) { assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`], + attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.finish_reason` + ], response.finishReason, ); } diff --git a/packages/instrumentation-cohere/tests/generate.test.ts b/packages/instrumentation-cohere/tests/generate.test.ts index 98d69f63..6548fa38 100644 --- a/packages/instrumentation-cohere/tests/generate.test.ts +++ b/packages/instrumentation-cohere/tests/generate.test.ts @@ -92,28 +92,31 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Cohere"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Cohere"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.user`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`], params.prompt, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.k); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TOP_P], params.p); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + params.p, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( @@ -125,7 +128,7 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { params.frequencyPenalty, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); @@ -134,11 +137,11 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { typeof response.meta?.billedUnits?.outputTokens === "number" ) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], response.meta?.billedUnits?.inputTokens, ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], response.meta?.billedUnits?.outputTokens, ); assert.strictEqual( @@ -148,11 +151,11 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { ); } assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], response.generations[0].text, ); }); @@ -169,28 +172,31 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Cohere"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Cohere"); assert.strictEqual( attributes[SpanAttributes.LLM_REQUEST_TYPE], "completion", ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.user`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`], params.prompt, ); assert.strictEqual(attributes[SpanAttributes.LLM_TOP_K], params.k); - assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TOP_P], params.p); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + params.p, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE], params.temperature, ); assert.strictEqual( @@ -202,7 +208,7 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { params.frequencyPenalty, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); @@ -214,11 +220,11 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { typeof response.meta?.billedUnits?.outputTokens === "number" ) { assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], response.meta?.billedUnits?.inputTokens, ); assert.strictEqual( - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], response.meta?.billedUnits?.outputTokens, ); assert.strictEqual( @@ -228,11 +234,11 @@ describe.skip("Test Generate with Cohere Instrumentation", () => { ); } assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], response.generations[0].text, ); } diff --git a/packages/instrumentation-cohere/tests/rerank.test.ts b/packages/instrumentation-cohere/tests/rerank.test.ts index 442c9ed2..a211a200 100644 --- a/packages/instrumentation-cohere/tests/rerank.test.ts +++ b/packages/instrumentation-cohere/tests/rerank.test.ts @@ -105,19 +105,19 @@ describe.skip("Test Rerank with Cohere Instrumentation", () => { const spans = memoryExporter.getFinishedSpans(); const attributes = spans[0].attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "Cohere"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Cohere"); assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TYPE], "rerank"); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.user`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`], params.query, ); assert.strictEqual( @@ -127,15 +127,15 @@ describe.skip("Test Rerank with Cohere Instrumentation", () => { : params.documents[1].text, ); assert.strictEqual( - attributes[SpanAttributes.LLM_REQUEST_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], params?.model ?? "command", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.relevanceScore`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.relevanceScore`], response.results[0].relevanceScore, ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], params.returnDocuments ? response.results[0].document?.text : response.results[0].index, diff --git a/packages/instrumentation-langchain/src/callback_handler.ts b/packages/instrumentation-langchain/src/callback_handler.ts index 815ea95c..4831e2ee 100644 --- a/packages/instrumentation-langchain/src/callback_handler.ts +++ b/packages/instrumentation-langchain/src/callback_handler.ts @@ -62,7 +62,7 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { const flatMessages = messages.flat(); span.setAttributes({ - [SpanAttributes.LLM_SYSTEM]: vendor, + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: vendor, [SpanAttributes.LLM_REQUEST_TYPE]: "chat", }); @@ -71,8 +71,8 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { flatMessages.forEach((message, idx) => { const role = this.mapMessageTypeToRole(message._getType()); span.setAttributes({ - [`${SpanAttributes.LLM_PROMPTS}.${idx}.role`]: role, - [`${SpanAttributes.LLM_PROMPTS}.${idx}.content`]: + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${idx}.role`]: role, + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${idx}.content`]: typeof message.content === "string" ? message.content : JSON.stringify(message.content), @@ -103,15 +103,15 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { }); span.setAttributes({ - [SpanAttributes.LLM_SYSTEM]: vendor, + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: vendor, [SpanAttributes.LLM_REQUEST_TYPE]: "completion", }); if (this.traceContent && prompts.length > 0) { prompts.forEach((prompt, idx) => { span.setAttributes({ - [`${SpanAttributes.LLM_PROMPTS}.${idx}.role`]: "user", - [`${SpanAttributes.LLM_PROMPTS}.${idx}.content`]: prompt, + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${idx}.role`]: "user", + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${idx}.content`]: prompt, }); }); } @@ -139,8 +139,9 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { output.generations.forEach((generation, idx) => { if (generation && generation.length > 0) { span.setAttributes({ - [`${SpanAttributes.LLM_COMPLETIONS}.${idx}.role`]: "assistant", - [`${SpanAttributes.LLM_COMPLETIONS}.${idx}.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${idx}.role`]: + "assistant", + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${idx}.content`]: generation[0].text, }); } @@ -152,8 +153,8 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { // Set both request and response model attributes like Python implementation span.setAttributes({ - [SpanAttributes.LLM_REQUEST_MODEL]: modelName || "unknown", - [SpanAttributes.LLM_RESPONSE_MODEL]: modelName || "unknown", + [SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL]: modelName || "unknown", + [SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL]: modelName || "unknown", }); // Add usage metrics if available @@ -161,12 +162,13 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { const usage = output.llmOutput.usage; if (usage.input_tokens) { span.setAttributes({ - [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: usage.input_tokens, + [SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]: usage.input_tokens, }); } if (usage.output_tokens) { span.setAttributes({ - [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: usage.output_tokens, + [SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]: + usage.output_tokens, }); } const totalTokens = @@ -183,12 +185,13 @@ export class TraceloopCallbackHandler extends BaseCallbackHandler { const usage = output.llmOutput.tokenUsage; if (usage.promptTokens) { span.setAttributes({ - [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: usage.promptTokens, + [SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]: usage.promptTokens, }); } if (usage.completionTokens) { span.setAttributes({ - [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: usage.completionTokens, + [SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]: + usage.completionTokens, }); } if (usage.totalTokens) { diff --git a/packages/instrumentation-langchain/test/instrumentation.test.ts b/packages/instrumentation-langchain/test/instrumentation.test.ts index 5f6641bb..d3d2db3b 100644 --- a/packages/instrumentation-langchain/test/instrumentation.test.ts +++ b/packages/instrumentation-langchain/test/instrumentation.test.ts @@ -432,30 +432,32 @@ describe("Test Langchain instrumentation", async function () { // Look for LLM span created by Bedrock instrumentation const llmSpan = spans.find( - (span) => span.attributes[SpanAttributes.LLM_SYSTEM] === "AWS", + (span) => span.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM] === "AWS", ); if (llmSpan) { // Test LLM span attributes like in amazon.test.ts const attributes = llmSpan.attributes; - assert.strictEqual(attributes[SpanAttributes.LLM_SYSTEM], "AWS"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS"); assert.strictEqual(attributes[SpanAttributes.LLM_REQUEST_TYPE], "chat"); - assert.ok(attributes[SpanAttributes.LLM_REQUEST_MODEL]); + assert.ok(attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL]); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "What is a popular landmark in the most populous city in the US?", ); assert.strictEqual( - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); - assert.ok(attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]); - assert.ok(attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS]); - assert.ok(attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]); + assert.ok( + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], + ); + assert.ok(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]); + assert.ok(attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]); assert.ok(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]); } else { // Test LangChain callback handler spans - now only creates completion span @@ -469,7 +471,7 @@ describe("Test Langchain instrumentation", async function () { // Test completion span attributes const completionAttributes = completionSpan.attributes; assert.strictEqual( - completionAttributes[SpanAttributes.LLM_SYSTEM], + completionAttributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "AWS", ); assert.strictEqual( @@ -477,12 +479,16 @@ describe("Test Langchain instrumentation", async function () { "chat", ); assert.strictEqual( - completionAttributes[SpanAttributes.LLM_REQUEST_MODEL], + completionAttributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "claude-3-7-sonnet", ); - assert.ok(completionAttributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS]); assert.ok( - completionAttributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + completionAttributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], + ); + assert.ok( + completionAttributes[ + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS + ], ); assert.ok(completionAttributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]); } diff --git a/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts b/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts index 35df069d..3479a5f7 100644 --- a/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts +++ b/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts @@ -49,14 +49,14 @@ export class CustomLLMInstrumentation { }); try { - span.setAttribute(SpanAttributes.LLM_SYSTEM, className); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_SYSTEM, className); span.setAttribute( - SpanAttributes.LLM_REQUEST_MODEL, + SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL, this.metadata.model, ); span.setAttribute(SpanAttributes.LLM_REQUEST_TYPE, "chat"); span.setAttribute( - SpanAttributes.LLM_REQUEST_TOP_P, + SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P, this.metadata.topP, ); if (shouldSendPrompts(plugin.config)) { @@ -64,7 +64,7 @@ export class CustomLLMInstrumentation { const content = messages[messageIdx].content; if (typeof content === "string") { span.setAttribute( - `${SpanAttributes.LLM_PROMPTS}.${messageIdx}.content`, + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${messageIdx}.content`, content as string, ); } else if ( @@ -72,13 +72,13 @@ export class CustomLLMInstrumentation { "text" ) { span.setAttribute( - `${SpanAttributes.LLM_PROMPTS}.${messageIdx}.content`, + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${messageIdx}.content`, (content as llamaindex.MessageContentTextDetail[])[0].text, ); } span.setAttribute( - `${SpanAttributes.LLM_PROMPTS}.${messageIdx}.role`, + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${messageIdx}.role`, messages[messageIdx].role, ); } @@ -134,7 +134,7 @@ export class CustomLLMInstrumentation { span: Span, metadata: llamaindex.LLMMetadata, ): T { - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, metadata.model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, metadata.model); if (!shouldSendPrompts(this.config)) { span.setStatus({ code: SpanStatusCode.OK }); @@ -145,18 +145,18 @@ export class CustomLLMInstrumentation { try { if ((result as llamaindex.ChatResponse).message) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`, (result as llamaindex.ChatResponse).message.role, ); const content = (result as llamaindex.ChatResponse).message.content; if (typeof content === "string") { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, content, ); } else if (content[0].type === "text") { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.0.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, content[0].text, ); } @@ -178,7 +178,7 @@ export class CustomLLMInstrumentation { execContext: Context, metadata: llamaindex.LLMMetadata, ): T { - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, metadata.model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, metadata.model); if (!shouldSendPrompts(this.config)) { span.setStatus({ code: SpanStatusCode.OK }); span.end(); @@ -186,7 +186,7 @@ export class CustomLLMInstrumentation { } return llmGeneratorWrapper(result, execContext, (message) => { - span.setAttribute(`${SpanAttributes.LLM_COMPLETIONS}.0.content`, message); + span.setAttribute(`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, message); span.setStatus({ code: SpanStatusCode.OK }); span.end(); }) as any; diff --git a/packages/instrumentation-openai/src/image-wrappers.ts b/packages/instrumentation-openai/src/image-wrappers.ts index e7b3af6e..b3b83aca 100644 --- a/packages/instrumentation-openai/src/image-wrappers.ts +++ b/packages/instrumentation-openai/src/image-wrappers.ts @@ -141,7 +141,7 @@ export function setImageGenerationRequestAttributes( const attributes: Attributes = {}; if (params.model) { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = params.model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; } if (params.size) { @@ -161,8 +161,8 @@ export function setImageGenerationRequestAttributes( } if (params.prompt) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = params.prompt; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; } Object.entries(attributes).forEach(([key, value]) => { @@ -180,7 +180,7 @@ export async function setImageEditRequestAttributes( const attributes: Attributes = {}; if (params.model) { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = params.model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; } if (params.size) { @@ -192,8 +192,8 @@ export async function setImageEditRequestAttributes( } if (params.prompt) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = params.prompt; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; } // Process input image if upload callback is available @@ -215,10 +215,10 @@ export async function setImageEditRequestAttributes( ); if (imageUrl) { - attributes[`${SpanAttributes.LLM_PROMPTS}.1.content`] = JSON.stringify([ + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.content`] = JSON.stringify([ { type: "image_url", image_url: { url: imageUrl } }, ]); - attributes[`${SpanAttributes.LLM_PROMPTS}.1.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.role`] = "user"; } } @@ -237,7 +237,7 @@ export async function setImageVariationRequestAttributes( const attributes: Attributes = {}; if (params.model) { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = params.model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; } if (params.size) { @@ -267,10 +267,10 @@ export async function setImageVariationRequestAttributes( ); if (imageUrl) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = JSON.stringify([ + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify([ { type: "image_url", image_url: { url: imageUrl } }, ]); - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; } } @@ -295,7 +295,7 @@ export async function setImageGenerationResponseAttributes( params, response.data.length, ); - attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS] = completionTokens; + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS] = completionTokens; // Calculate prompt tokens if enrichTokens is enabled if (instrumentationConfig?.enrichTokens) { @@ -311,7 +311,7 @@ export async function setImageGenerationResponseAttributes( } if (estimatedPromptTokens > 0) { - attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS] = estimatedPromptTokens; } @@ -340,9 +340,9 @@ export async function setImageGenerationResponseAttributes( firstImage.b64_json, ); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([{ type: "image_url", image_url: { url: imageUrl } }]); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; } catch (error) { console.error("Failed to upload generated image:", error); } @@ -363,25 +363,25 @@ export async function setImageGenerationResponseAttributes( base64Data, ); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([ { type: "image_url", image_url: { url: uploadedUrl } }, ]); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; } catch (error) { console.error("Failed to fetch and upload generated image:", error); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([ { type: "image_url", image_url: { url: firstImage.url } }, ]); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; } } else if (firstImage.url) { - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([ { type: "image_url", image_url: { url: firstImage.url } }, ]); - attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; } if (firstImage.revised_prompt) { @@ -408,7 +408,7 @@ export function wrapImageGeneration( const span = tracer.startSpan("openai.images.generate", { kind: SpanKind.CLIENT, attributes: { - [SpanAttributes.LLM_SYSTEM]: "OpenAI", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "OpenAI", "gen_ai.request.type": "image_generation", }, }); @@ -467,7 +467,7 @@ export function wrapImageEdit( const span = tracer.startSpan("openai.images.edit", { kind: SpanKind.CLIENT, attributes: { - [SpanAttributes.LLM_SYSTEM]: "OpenAI", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "OpenAI", "gen_ai.request.type": "image_edit", }, }); @@ -534,7 +534,7 @@ export function wrapImageVariation( const span = tracer.startSpan("openai.images.createVariation", { kind: SpanKind.CLIENT, attributes: { - [SpanAttributes.LLM_SYSTEM]: "OpenAI", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "OpenAI", "gen_ai.request.type": "image_variation", }, }); diff --git a/packages/instrumentation-openai/src/instrumentation.ts b/packages/instrumentation-openai/src/instrumentation.ts index f825054c..06965bd0 100644 --- a/packages/instrumentation-openai/src/instrumentation.ts +++ b/packages/instrumentation-openai/src/instrumentation.ts @@ -298,20 +298,20 @@ export class OpenAIInstrumentation extends InstrumentationBase { const { provider } = this._detectVendorFromURL(client); const attributes: Attributes = { - [SpanAttributes.LLM_SYSTEM]: provider, + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: provider, [SpanAttributes.LLM_REQUEST_TYPE]: type, }; try { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = params.model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; if (params.max_tokens) { - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = params.max_tokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; } if (params.temperature) { - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; } if (params.top_p) { - attributes[SpanAttributes.LLM_REQUEST_TOP_P] = params.top_p; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p; } if (params.frequency_penalty) { attributes[SpanAttributes.LLM_FREQUENCY_PENALTY] = @@ -335,13 +335,13 @@ export class OpenAIInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { if (type === "chat") { params.messages.forEach((message, index) => { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = message.role; if (typeof message.content === "string") { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = (message.content as string) || ""; } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = JSON.stringify(message.content); } }); @@ -376,12 +376,12 @@ export class OpenAIInstrumentation extends InstrumentationBase { ] = JSON.stringify(tool.function.parameters); }); } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; if (typeof params.prompt === "string") { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify(params.prompt); } } @@ -653,18 +653,18 @@ export class OpenAIInstrumentation extends InstrumentationBase { | { span: Span; type: "chat"; result: ChatCompletion } | { span: Span; type: "completion"; result: Completion }) { try { - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, result.model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result.model); if (result.usage) { span.setAttribute( SpanAttributes.LLM_USAGE_TOTAL_TOKENS, result.usage?.total_tokens, ); span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result.usage?.completion_tokens, ); span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result.usage?.prompt_tokens, ); } @@ -673,25 +673,25 @@ export class OpenAIInstrumentation extends InstrumentationBase { if (type === "chat") { result.choices.forEach((choice, index) => { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`, choice.finish_reason, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, choice.message.role, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, choice.message.content ?? "", ); if (choice.message.function_call) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.function_call.name`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.function_call.name`, choice.message.function_call.name, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.function_call.arguments`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.function_call.arguments`, choice.message.function_call.arguments, ); } @@ -701,11 +701,11 @@ export class OpenAIInstrumentation extends InstrumentationBase { ] of choice?.message?.tool_calls?.entries() || []) { if (toolCall.type === "function" && "function" in toolCall) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.tool_calls.${toolIndex}.name`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.tool_calls.${toolIndex}.name`, toolCall.function.name, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.tool_calls.${toolIndex}.arguments`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.tool_calls.${toolIndex}.arguments`, toolCall.function.arguments, ); } @@ -714,15 +714,15 @@ export class OpenAIInstrumentation extends InstrumentationBase { } else { result.choices.forEach((choice, index) => { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`, choice.finish_reason, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, choice.text, ); }); diff --git a/packages/instrumentation-openai/test/instrumentation.test.ts b/packages/instrumentation-openai/test/instrumentation.test.ts index f83359f9..37dc3f31 100644 --- a/packages/instrumentation-openai/test/instrumentation.test.ts +++ b/packages/instrumentation-openai/test/instrumentation.test.ts @@ -148,23 +148,27 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.ok( completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], "15", ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -189,27 +193,33 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` + ], result, ); assert.ok( completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], "8", ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -234,35 +244,43 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` + ], result, ); assert.ok( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], ); assert.ok( completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ], ); assert.ok( completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], "8", ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -281,11 +299,13 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); }); @@ -310,11 +330,13 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); }); @@ -398,11 +420,13 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "What's the weather like in Boston?", ); assert.strictEqual( @@ -435,14 +459,14 @@ describe("Test OpenAI instrumentation", async function () { ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.function_call.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` ], "get_current_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.function_call.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` ]! as string, ), { location: "Boston" }, @@ -451,12 +475,14 @@ describe("Test OpenAI instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], 82, ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -498,11 +524,13 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "What's the weather like in Boston?", ); assert.strictEqual( @@ -535,13 +563,13 @@ describe("Test OpenAI instrumentation", async function () { ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.name` ], "get_current_weather", ); const parsedArgs = JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.arguments` ]! as string, ); // API returns either "Boston" or "Boston, MA" depending on the call @@ -552,12 +580,14 @@ describe("Test OpenAI instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], 82, ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -631,28 +661,28 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(completionSpan); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.name` ], "get_current_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.0.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.arguments` ]! as string, ), { location: "Boston, MA" }, ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.1.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.name` ], "get_tomorrow_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.1.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.arguments` ]! as string, ), { location: "Chicago, IL" }, @@ -676,7 +706,7 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(imageSpan); assert.strictEqual( - imageSpan.attributes[SpanAttributes.LLM_SYSTEM], + imageSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); assert.strictEqual( @@ -684,7 +714,7 @@ describe("Test OpenAI instrumentation", async function () { "image_generation", ); assert.strictEqual( - imageSpan.attributes[SpanAttributes.LLM_REQUEST_MODEL], + imageSpan.attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "dall-e-2", ); assert.strictEqual( @@ -693,24 +723,28 @@ describe("Test OpenAI instrumentation", async function () { ); assert.strictEqual(imageSpan.attributes["gen_ai.request.image.count"], 1); assert.strictEqual( - imageSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + imageSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "A test image", ); assert.strictEqual( - imageSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + imageSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); // Check token usage calculation (dall-e-2 1024x1024 should be ~1056 tokens) - assert.ok(imageSpan.attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]); + assert.ok( + imageSpan.attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], + ); assert.ok(imageSpan.attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]); // Check response content assert.ok( - imageSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + imageSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` + ], ); assert.strictEqual( - imageSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + imageSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); }); @@ -736,7 +770,7 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(editSpan); assert.strictEqual( - editSpan.attributes[SpanAttributes.LLM_SYSTEM], + editSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); assert.strictEqual( @@ -750,27 +784,27 @@ describe("Test OpenAI instrumentation", async function () { ); assert.strictEqual(editSpan.attributes["gen_ai.request.image.count"], 1); assert.strictEqual( - editSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + editSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Add a red hat", ); assert.strictEqual( - editSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + editSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); // Check token usage calculation assert.strictEqual( - editSpan.attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + editSpan.attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], 4160, ); assert.ok(editSpan.attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]); // Should include prompt tokens // Check response content assert.ok( - editSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + editSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], ); assert.strictEqual( - editSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + editSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], "assistant", ); }); @@ -797,7 +831,7 @@ describe("Test OpenAI instrumentation", async function () { assert.ok(variationSpan); assert.strictEqual( - variationSpan.attributes[SpanAttributes.LLM_SYSTEM], + variationSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); assert.strictEqual( @@ -816,17 +850,23 @@ describe("Test OpenAI instrumentation", async function () { // Check token usage calculation (DALL-E 2 1024x1024 = 1056 tokens) assert.strictEqual( - variationSpan.attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + variationSpan.attributes[ + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS + ], 1056, ); assert.ok(variationSpan.attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]); // Should include estimated input tokens // Check response content assert.ok( - variationSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + variationSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` + ], ); assert.strictEqual( - variationSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + variationSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role` + ], "assistant", ); }); @@ -853,13 +893,13 @@ describe("Test OpenAI instrumentation", async function () { const dalle2Span = spans.find( (span) => span.name === "openai.images.generate" && - span.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] === + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === "Test standard quality", ); const dalle3Span = spans.find( (span) => span.name === "openai.images.generate" && - span.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] === + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === "Test HD quality", ); @@ -868,13 +908,13 @@ describe("Test OpenAI instrumentation", async function () { // DALL-E 2 standard should be 1056 tokens assert.strictEqual( - dalle2Span.attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + dalle2Span.attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], 1056, ); // DALL-E 3 HD should be 4160 tokens assert.strictEqual( - dalle3Span.attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS], + dalle3Span.attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], 4160, ); }); diff --git a/packages/instrumentation-together/src/instrumentation.ts b/packages/instrumentation-together/src/instrumentation.ts index 954a0439..3063b430 100644 --- a/packages/instrumentation-together/src/instrumentation.ts +++ b/packages/instrumentation-together/src/instrumentation.ts @@ -187,20 +187,20 @@ export class TogetherInstrumentation extends InstrumentationBase { }; }): Span { const attributes: Attributes = { - [SpanAttributes.LLM_SYSTEM]: "TogetherAI", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "TogetherAI", [SpanAttributes.LLM_REQUEST_TYPE]: type, }; try { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = params.model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; if (params.max_tokens) { - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = params.max_tokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; } if (params.temperature) { - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; } if (params.top_p) { - attributes[SpanAttributes.LLM_REQUEST_TOP_P] = params.top_p; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p; } if (params.frequency_penalty) { attributes[SpanAttributes.LLM_FREQUENCY_PENALTY] = @@ -223,13 +223,13 @@ export class TogetherInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { if (type === "chat") { params.messages.forEach((message, index) => { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = message.role; if (typeof message.content === "string") { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = (message.content as string) || ""; } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = JSON.stringify(message.content); } }); @@ -261,12 +261,12 @@ export class TogetherInstrumentation extends InstrumentationBase { ] = JSON.stringify(tool.function.parameters); }); } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; if (typeof params.prompt === "string") { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; } else { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify(params.prompt); } } @@ -495,7 +495,7 @@ export class TogetherInstrumentation extends InstrumentationBase { | { span: Span; type: "chat"; result: ChatCompletion } | { span: Span; type: "completion"; result: Completion }) { try { - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, result.model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result.model); if (result.usage) { span.setAttribute( @@ -503,11 +503,11 @@ export class TogetherInstrumentation extends InstrumentationBase { result.usage?.total_tokens, ); span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result.usage?.completion_tokens, ); span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result.usage?.prompt_tokens, ); } @@ -516,34 +516,34 @@ export class TogetherInstrumentation extends InstrumentationBase { if (type === "chat") { result.choices.forEach((choice, index) => { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`, choice.finish_reason ?? "", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, choice.message?.role ?? "", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, choice.message?.content ?? "", ); if (choice.message?.function_call) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.function_call.name`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.function_call.name`, choice.message?.function_call?.name ?? "", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.function_call.arguments`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.function_call.arguments`, choice.message?.function_call?.arguments ?? "", ); } else if (choice.message?.tool_calls?.[0]) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.function_call.name`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.function_call.name`, choice.message.tool_calls[0].function.name ?? "", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.function_call.arguments`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.function_call.arguments`, choice.message.tool_calls[0].function.arguments ?? "", ); } @@ -554,11 +554,11 @@ export class TogetherInstrumentation extends InstrumentationBase { toolCall, ] of choice.message.tool_calls.entries()) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.tool_calls.${toolIndex}.name`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.tool_calls.${toolIndex}.name`, toolCall.function.name, ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.tool_calls.${toolIndex}.arguments`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.tool_calls.${toolIndex}.arguments`, toolCall.function.arguments, ); } @@ -567,15 +567,15 @@ export class TogetherInstrumentation extends InstrumentationBase { } else { result.choices.forEach((choice, index) => { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`, choice.finish_reason ?? "", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, choice.text ?? "", ); }); diff --git a/packages/instrumentation-together/test/instrumentation.test.ts b/packages/instrumentation-together/test/instrumentation.test.ts index 3324c435..a14f05f2 100644 --- a/packages/instrumentation-together/test/instrumentation.test.ts +++ b/packages/instrumentation-together/test/instrumentation.test.ts @@ -127,24 +127,26 @@ describe("Test Together instrumentation", async function () { "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "What's the weather like in Boston?", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name` + `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name` ], "get_current_weather", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.description` + `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.description` ], "Get the current weather in a given location", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.arguments` + `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.arguments` ], JSON.stringify({ type: "object", @@ -176,7 +178,9 @@ describe("Test Together instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], 333, ); assert.ok( @@ -204,18 +208,24 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role` + ], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.ok( completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], 37, ); assert.ok( @@ -254,11 +264,15 @@ describe("Test Together instrumentation", async function () { assert.ok(result); assert.ok(completionSpan); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role` + ], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.strictEqual( @@ -271,7 +285,9 @@ describe("Test Together instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], 37, ); assert.ok( @@ -304,7 +320,9 @@ describe("Test Together instrumentation", async function () { "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.ok( @@ -316,7 +334,9 @@ describe("Test Together instrumentation", async function () { ]! > 0, ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role` + ], "assistant", ); assert.ok( @@ -361,7 +381,9 @@ describe("Test Together instrumentation", async function () { "Prompt role should be 'user'", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", "Prompt content should match input", ); @@ -376,7 +398,9 @@ describe("Test Together instrumentation", async function () { "Completion tokens should be greater than 0", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role` + ], "assistant", "Completion role should be 'assistant'", ); @@ -434,24 +458,26 @@ describe("Test Together instrumentation", async function () { "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "What's the weather like in Boston?", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name` + `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name` ], "get_current_weather", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.description` + `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.description` ], "Get the current weather in a given location", ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.arguments` + `${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.arguments` ], JSON.stringify({ type: "object", @@ -483,7 +509,9 @@ describe("Test Together instrumentation", async function () { completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], 333, ); assert.ok( diff --git a/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts b/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts index a2d50676..bc4f701c 100644 --- a/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts +++ b/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts @@ -157,7 +157,7 @@ export class AIPlatformInstrumentation extends InstrumentationBase { | undefined; }): Span { const attributes: Attributes = { - [SpanAttributes.LLM_SYSTEM]: "Google", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "Google", [SpanAttributes.LLM_REQUEST_TYPE]: "completion", }; @@ -165,22 +165,22 @@ export class AIPlatformInstrumentation extends InstrumentationBase { if (params !== undefined) { if (params.endpoint) { const model = params.endpoint.split("/").pop(); - attributes[SpanAttributes.LLM_REQUEST_MODEL] = model; - attributes[SpanAttributes.LLM_RESPONSE_MODEL] = model; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = model; + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL] = model; } if (params?.parameters) { if ( params?.parameters.structValue?.fields?.maxOutputTokens.numberValue ) { - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params?.parameters.structValue?.fields?.maxOutputTokens.numberValue; } if (params?.parameters.structValue?.fields?.temperature.numberValue) { - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params?.parameters.structValue?.fields?.temperature.numberValue; } if (params?.parameters.structValue?.fields?.topP.numberValue) { - attributes[SpanAttributes.LLM_REQUEST_TOP_P] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params?.parameters.structValue?.fields?.topP.numberValue; } if (params?.parameters.structValue?.fields?.topK.numberValue) { @@ -199,18 +199,18 @@ export class AIPlatformInstrumentation extends InstrumentationBase { "prompt" in params.instances[0].structValue.fields && params.instances[0].structValue?.fields?.prompt.stringValue ) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user"; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.instances[0].structValue?.fields?.prompt.stringValue; } else if ( params.instances[0].structValue && params.instances[0].structValue.fields?.messages.listValue ?.values?.[0].structValue?.fields?.content.stringValue ) { - attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = params.instances[0].structValue.fields?.messages.listValue ?.values?.[0].structValue?.fields?.author.stringValue ?? "user"; - attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.instances[0].structValue.fields?.messages.listValue?.values?.[0].structValue?.fields?.content.stringValue; } } @@ -271,7 +271,7 @@ export class AIPlatformInstrumentation extends InstrumentationBase { }) { try { if (result[0].model) - span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, result[0].model); + span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result[0].model); if (result) { if (result[0].metadata) { @@ -281,7 +281,7 @@ export class AIPlatformInstrumentation extends InstrumentationBase { ?.totalTokens.numberValue === "number" ) span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result[0].metadata?.structValue?.fields?.tokenMetadata.structValue ?.fields?.outputTokenCount.structValue?.fields?.totalTokens .numberValue, @@ -293,7 +293,7 @@ export class AIPlatformInstrumentation extends InstrumentationBase { ?.totalTokens.numberValue === "number" ) span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result[0].metadata?.structValue?.fields?.tokenMetadata.structValue ?.fields?.inputTokenCount.structValue?.fields?.totalTokens .numberValue, @@ -326,12 +326,12 @@ export class AIPlatformInstrumentation extends InstrumentationBase { !!prediction.structValue?.fields?.content.stringValue ) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, prediction.structValue?.fields?.content.stringValue, ); } else if ( @@ -341,12 +341,12 @@ export class AIPlatformInstrumentation extends InstrumentationBase { ?.values?.[0]?.structValue?.fields?.content.stringValue ) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, prediction.structValue?.fields?.candidates.listValue ?.values?.[0]?.structValue?.fields?.content.stringValue, ); diff --git a/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts b/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts index ea95039d..bab5bd20 100644 --- a/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts +++ b/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts @@ -137,20 +137,20 @@ export class VertexAIInstrumentation extends InstrumentationBase { params: vertexAI.GenerateContentRequest; }): Span { const attributes: Attributes = { - [SpanAttributes.LLM_SYSTEM]: "Google", + [SpanAttributes.ATTR_GEN_AI_SYSTEM]: "Google", [SpanAttributes.LLM_REQUEST_TYPE]: "completion", }; try { - attributes[SpanAttributes.LLM_REQUEST_MODEL] = instance["model"]; - attributes[SpanAttributes.LLM_RESPONSE_MODEL] = instance["model"]; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = instance["model"]; + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL] = instance["model"]; if (instance["generationConfig"]) { - attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = instance["generationConfig"].max_output_tokens; - attributes[SpanAttributes.LLM_REQUEST_TEMPERATURE] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = instance["generationConfig"].temperature; - attributes[SpanAttributes.LLM_REQUEST_TOP_P] = + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = instance["generationConfig"].top_p; attributes[SpanAttributes.LLM_TOP_K] = instance["generationConfig"].top_k; @@ -160,17 +160,17 @@ export class VertexAIInstrumentation extends InstrumentationBase { let i = 0; if (instance["systemInstruction"]) { - attributes[`${SpanAttributes.LLM_PROMPTS}.${i}.role`] = "system"; - attributes[`${SpanAttributes.LLM_PROMPTS}.${i}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i}.role`] = "system"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i}.content`] = this._formatPartsData(instance["systemInstruction"].parts); i++; } params.contents.forEach((content, j) => { - attributes[`${SpanAttributes.LLM_PROMPTS}.${i + j}.role`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i + j}.role`] = content.role ?? "user"; - attributes[`${SpanAttributes.LLM_PROMPTS}.${i + j}.content`] = + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i + j}.content`] = this._formatPartsData(content.parts); }); } @@ -230,13 +230,13 @@ export class VertexAIInstrumentation extends InstrumentationBase { if (streamResponse.usageMetadata?.candidatesTokenCount) span.setAttribute( - SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, streamResponse.usageMetadata.candidatesTokenCount, ); if (streamResponse.usageMetadata?.promptTokenCount) span.setAttribute( - SpanAttributes.LLM_USAGE_PROMPT_TOKENS, + SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS, streamResponse.usageMetadata.promptTokenCount, ); @@ -244,18 +244,18 @@ export class VertexAIInstrumentation extends InstrumentationBase { streamResponse.candidates?.forEach((candidate, index) => { if (candidate.finishReason) span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`, candidate.finishReason, ); if (candidate.content) { span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.role`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`, candidate.content.role ?? "assistant", ); span.setAttribute( - `${SpanAttributes.LLM_COMPLETIONS}.${index}.content`, + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`, this._formatPartsData(candidate.content.parts), ); } diff --git a/packages/instrumentation-vertexai/tests/gemini.test.ts b/packages/instrumentation-vertexai/tests/gemini.test.ts index c8a06f3c..09c09cee 100644 --- a/packages/instrumentation-vertexai/tests/gemini.test.ts +++ b/packages/instrumentation-vertexai/tests/gemini.test.ts @@ -84,13 +84,22 @@ describe.skip("Test Gemini GenerativeModel Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes["gen_ai.system"], "Google"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Google"); assert.strictEqual(attributes["llm.request.type"], "completion"); - assert.strictEqual(attributes["gen_ai.request.model"], model); - assert.strictEqual(attributes["gen_ai.request.top_p"], 0.9); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + 0.9, + ); assert.strictEqual(attributes["gen_ai.prompt.0.content"], prompt); assert.strictEqual(attributes["gen_ai.prompt.0.role"], "user"); - assert.strictEqual(attributes["gen_ai.response.model"], model); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], + model, + ); assert.strictEqual(attributes["gen_ai.completion.0.role"], "model"); assert.strictEqual( attributes["gen_ai.completion.0.content"], @@ -137,14 +146,26 @@ describe.skip("Test Gemini GenerativeModel Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes["gen_ai.system"], "Google"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Google"); assert.strictEqual(attributes["llm.request.type"], "completion"); - assert.strictEqual(attributes["gen_ai.request.model"], model); - assert.strictEqual(attributes["gen_ai.request.top_p"], 0.9); - assert.strictEqual(attributes["gen_ai.request.max_tokens"], 256); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + 0.9, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS], + 256, + ); assert.strictEqual(attributes["gen_ai.prompt.0.content"], prompt); assert.strictEqual(attributes["gen_ai.prompt.0.role"], "user"); - assert.strictEqual(attributes["gen_ai.response.model"], model); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], + model, + ); assert.strictEqual(attributes["gen_ai.completion.0.role"], "model"); fullTextResponse.forEach((resp, index) => { diff --git a/packages/instrumentation-vertexai/tests/palm2.test.ts b/packages/instrumentation-vertexai/tests/palm2.test.ts index 1e28aa04..b2b51e21 100644 --- a/packages/instrumentation-vertexai/tests/palm2.test.ts +++ b/packages/instrumentation-vertexai/tests/palm2.test.ts @@ -92,14 +92,23 @@ describe.skip("Test PaLM2 PredictionServiceClient Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes["gen_ai.system"], "Google"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Google"); assert.strictEqual(attributes["llm.request.type"], "completion"); - assert.strictEqual(attributes["gen_ai.request.model"], model); - assert.strictEqual(attributes["gen_ai.request.top_p"], parameter.topP); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + parameter.topP, + ); assert.strictEqual(attributes["llm.top_k"], parameter.topK); assert.strictEqual(attributes["gen_ai.prompt.0.content"], prompt.prompt); assert.strictEqual(attributes["gen_ai.prompt.0.role"], "user"); - assert.strictEqual(attributes["gen_ai.response.model"], model); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], + model, + ); assert.strictEqual(attributes["gen_ai.completion.0.role"], "assistant"); assert.strictEqual( attributes["gen_ai.completion.0.content"], @@ -165,17 +174,26 @@ describe.skip("Test PaLM2 PredictionServiceClient Instrumentation", () => { const attributes = spans[0].attributes; - assert.strictEqual(attributes["gen_ai.system"], "Google"); + assert.strictEqual(attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Google"); assert.strictEqual(attributes["llm.request.type"], "completion"); - assert.strictEqual(attributes["gen_ai.request.model"], model); - assert.strictEqual(attributes["gen_ai.request.top_p"], parameter.topP); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], + model, + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P], + parameter.topP, + ); assert.strictEqual(attributes["llm.top_k"], parameter.topK); assert.strictEqual( attributes["gen_ai.prompt.0.content"], prompt.messages[0].content, ); assert.strictEqual(attributes["gen_ai.prompt.0.role"], "user"); - assert.strictEqual(attributes["gen_ai.response.model"], model); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], + model, + ); assert.strictEqual(attributes["gen_ai.completion.0.role"], "assistant"); assert.strictEqual( attributes["gen_ai.completion.0.content"], 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 bbf8a0e5..f816f861 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -372,15 +372,14 @@ const transformPrompts = (attributes: Record): void => { const transformPromptTokens = (attributes: Record): void => { if ( - !(SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS in attributes) && + !(SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS in attributes) && AI_USAGE_PROMPT_TOKENS in attributes ) { - attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS] = + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS] = attributes[AI_USAGE_PROMPT_TOKENS]; } delete attributes[AI_USAGE_PROMPT_TOKENS]; - delete attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS]; }; const transformCompletionTokens = (attributes: Record): void => { @@ -393,7 +392,6 @@ const transformCompletionTokens = (attributes: Record): void => { } delete attributes[AI_USAGE_COMPLETION_TOKENS]; - delete attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS]; }; const transformProviderMetadata = (attributes: Record): void => { @@ -463,18 +461,24 @@ const transformVendor = (attributes: Record): void => { let mappedVendor = null; if (typeof vendor === "string" && vendor.length > 0) { const providerName = vendor.split(".")[0]; + + // Set the standard gen_ai.provider.name attribute with lowercase provider name attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = providerName; + // Find the mapped vendor for backward compatibility with deprecated gen_ai.system for (const prefix of Object.keys(VENDOR_MAPPING)) { if (vendor.startsWith(prefix)) { mappedVendor = VENDOR_MAPPING[prefix]; break; } } + + // Set deprecated gen_ai.system attribute for backward compatibility + if (mappedVendor) { + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM] = mappedVendor; + } } - attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME] = - mappedVendor || vendor; delete attributes[AI_MODEL_PROVIDER]; } }; diff --git a/packages/traceloop-sdk/src/lib/tracing/decorators.ts b/packages/traceloop-sdk/src/lib/tracing/decorators.ts index fb30ca25..84a69e4e 100644 --- a/packages/traceloop-sdk/src/lib/tracing/decorators.ts +++ b/packages/traceloop-sdk/src/lib/tracing/decorators.ts @@ -102,7 +102,7 @@ function withEntity< const agentName = entityContext.getValue(AGENT_NAME_KEY); if (agentName) { span.setAttribute( - SpanAttributes.GEN_AI_AGENT_NAME, + SpanAttributes.ATTR_GEN_AI_AGENT_NAME, agentName as string, ); } diff --git a/packages/traceloop-sdk/src/lib/tracing/manual.ts b/packages/traceloop-sdk/src/lib/tracing/manual.ts index bd7fff93..3a0fa939 100644 --- a/packages/traceloop-sdk/src/lib/tracing/manual.ts +++ b/packages/traceloop-sdk/src/lib/tracing/manual.ts @@ -79,13 +79,13 @@ export class LLMSpan { }[]; }) { this.span.setAttributes({ - [SpanAttributes.LLM_REQUEST_MODEL]: model, + [SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL]: model, }); messages.forEach((message, index) => { this.span.setAttributes({ - [`${SpanAttributes.LLM_PROMPTS}.${index}.role`]: message.role, - [`${SpanAttributes.LLM_PROMPTS}.${index}.content`]: + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`]: message.role, + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`]: typeof message.content === "string" ? message.content : JSON.stringify(message.content), @@ -112,23 +112,24 @@ export class LLMSpan { }; }[]; }) { - this.span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, model); + this.span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, model); if (usage) { this.span.setAttributes({ - [SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: usage.prompt_tokens, - [SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: usage.completion_tokens, + [SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: usage.prompt_tokens, + [SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: + usage.completion_tokens, [SpanAttributes.LLM_USAGE_TOTAL_TOKENS]: usage.total_tokens, }); } completions?.forEach((completion, index) => { this.span.setAttributes({ - [`${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`]: completion.finish_reason, - [`${SpanAttributes.LLM_COMPLETIONS}.${index}.role`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.role`]: completion.message.role, - [`${SpanAttributes.LLM_COMPLETIONS}.${index}.content`]: + [`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.${index}.content`]: completion.message.content || "", }); }); @@ -149,7 +150,7 @@ export function withVectorDBCall< const agentName = entityContext.getValue(AGENT_NAME_KEY); if (agentName) { span.setAttribute( - SpanAttributes.GEN_AI_AGENT_NAME, + SpanAttributes.ATTR_GEN_AI_AGENT_NAME, agentName as string, ); } @@ -178,7 +179,10 @@ export function withLLMCall< // Set agent name if there's an active agent context const agentName = currentContext.getValue(AGENT_NAME_KEY); if (agentName) { - span.setAttribute(SpanAttributes.GEN_AI_AGENT_NAME, agentName as string); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_AGENT_NAME, + agentName as string, + ); } trace.setSpan(currentContext, span); diff --git a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts index ade8993a..025f4345 100644 --- a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts +++ b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts @@ -186,7 +186,10 @@ const onSpanStart = (span: Span): void => { const agentName = context.active().getValue(AGENT_NAME_KEY); if (agentName) { - span.setAttribute(SpanAttributes.GEN_AI_AGENT_NAME, agentName as string); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_AGENT_NAME, + agentName as string, + ); } const associationProperties = context @@ -271,7 +274,7 @@ const onSpanEnd = ( // Handle agent name propagation for AI SDK spans const traceId = span.spanContext().traceId; - const agentName = span.attributes[SpanAttributes.GEN_AI_AGENT_NAME]; + const agentName = span.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME]; if (agentName && typeof agentName === "string") { // Store agent name for this trace with current timestamp @@ -281,7 +284,7 @@ const onSpanEnd = ( }); } else if (!agentName && traceAgentNames.has(traceId)) { // This span doesn't have agent name but trace does - propagate it - span.attributes[SpanAttributes.GEN_AI_AGENT_NAME] = + span.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME] = traceAgentNames.get(traceId)!.agentName; } diff --git a/packages/traceloop-sdk/test/agent_decorator.test.ts b/packages/traceloop-sdk/test/agent_decorator.test.ts index 3a806aff..0ef4f6c8 100644 --- a/packages/traceloop-sdk/test/agent_decorator.test.ts +++ b/packages/traceloop-sdk/test/agent_decorator.test.ts @@ -136,15 +136,15 @@ describe("Test Agent Decorator", () => { "plan_trip", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.GEN_AI_AGENT_NAME}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_AGENT_NAME}`], "plan_trip", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); }); @@ -197,15 +197,15 @@ describe("Test Agent Decorator", () => { "travel_planner", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.GEN_AI_AGENT_NAME}`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_AGENT_NAME}`], "travel_planner", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); }); @@ -259,7 +259,7 @@ describe("Test Agent Decorator", () => { "123", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.GEN_AI_AGENT_NAME}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_AGENT_NAME}`], "assistant", ); assert.strictEqual( @@ -267,15 +267,17 @@ describe("Test Agent Decorator", () => { "chat", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL}`], "gpt-3.5-turbo", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); }); diff --git a/packages/traceloop-sdk/test/ai-sdk-agent-integration.test.ts b/packages/traceloop-sdk/test/ai-sdk-agent-integration.test.ts index 499078fd..ee032ce7 100644 --- a/packages/traceloop-sdk/test/ai-sdk-agent-integration.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-agent-integration.test.ts @@ -162,7 +162,7 @@ describe("Test AI SDK Agent Integration with Recording", function () { // Verify root span has agent attributes assert.strictEqual( - rootSpan.attributes[SpanAttributes.GEN_AI_AGENT_NAME], + rootSpan.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "test_calculator_agent", "Root span should have agent name", ); @@ -200,7 +200,7 @@ describe("Test AI SDK Agent Integration with Recording", function () { // Verify tool call span inherits agent name if (toolSpan) { assert.strictEqual( - toolSpan.attributes[SpanAttributes.GEN_AI_AGENT_NAME], + toolSpan.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "test_calculator_agent", "Tool span should inherit agent name from parent", ); @@ -218,7 +218,7 @@ describe("Test AI SDK Agent Integration with Recording", function () { // Verify child LLM span inherits agent name if (childLLMSpan) { assert.strictEqual( - childLLMSpan.attributes[SpanAttributes.GEN_AI_AGENT_NAME], + childLLMSpan.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "test_calculator_agent", "Child LLM span should inherit agent name from parent", ); @@ -306,7 +306,7 @@ describe("Test AI SDK Agent Integration with Recording", function () { // Verify root span does NOT have agent attributes assert.strictEqual( - rootSpan.attributes[SpanAttributes.GEN_AI_AGENT_NAME], + rootSpan.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], undefined, "Root span should not have agent name when no agent metadata", ); @@ -361,7 +361,7 @@ describe("Test AI SDK Agent Integration with Recording", function () { // Verify root span has agent attributes assert.strictEqual( - rootSpan.attributes[SpanAttributes.GEN_AI_AGENT_NAME], + rootSpan.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "profile_generator_agent", "Root span should have agent name", ); @@ -420,7 +420,7 @@ describe("Test AI SDK Agent Integration with Recording", function () { // Verify root span has agent attributes assert.strictEqual( - rootSpan.attributes[SpanAttributes.GEN_AI_AGENT_NAME], + rootSpan.attributes[SpanAttributes.ATTR_GEN_AI_AGENT_NAME], "poetry_agent", "Root span should have agent name", ); diff --git a/packages/traceloop-sdk/test/ai-sdk-integration.test.ts b/packages/traceloop-sdk/test/ai-sdk-integration.test.ts index 43969095..01bb8202 100644 --- a/packages/traceloop-sdk/test/ai-sdk-integration.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-integration.test.ts @@ -118,11 +118,14 @@ describe("Test AI SDK Integration with Recording", function () { assert.strictEqual(generateTextSpan.name, "text.generate"); // Verify vendor - assert.strictEqual(generateTextSpan.attributes["gen_ai.system"], "OpenAI"); + assert.strictEqual( + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "OpenAI", + ); // Verify model information assert.strictEqual( - generateTextSpan.attributes["gen_ai.request.model"], + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "gpt-3.5-turbo", ); @@ -186,11 +189,14 @@ describe("Test AI SDK Integration with Recording", function () { assert.strictEqual(generateTextSpan.name, "text.generate"); // Verify vendor - assert.strictEqual(generateTextSpan.attributes["gen_ai.system"], "Google"); + assert.strictEqual( + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "Google", + ); // Verify model information assert.strictEqual( - generateTextSpan.attributes["gen_ai.request.model"], + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "gemini-1.5-flash", ); @@ -240,9 +246,9 @@ describe("Test AI SDK Integration with Recording", function () { assert.ok(aiSdkSpan); // Verify LLM_INPUT_MESSAGES attribute exists and is valid JSON - assert.ok(aiSdkSpan.attributes[SpanAttributes.LLM_INPUT_MESSAGES]); + assert.ok(aiSdkSpan.attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES]); const inputMessages = JSON.parse( - aiSdkSpan.attributes[SpanAttributes.LLM_INPUT_MESSAGES] as string, + aiSdkSpan.attributes[SpanAttributes.ATTR_GEN_AI_INPUT_MESSAGES] as string, ); assert.ok(Array.isArray(inputMessages)); assert.strictEqual(inputMessages.length, 1); @@ -257,9 +263,11 @@ describe("Test AI SDK Integration with Recording", function () { ); // Verify LLM_OUTPUT_MESSAGES attribute exists and is valid JSON - assert.ok(aiSdkSpan.attributes[SpanAttributes.LLM_OUTPUT_MESSAGES]); + assert.ok(aiSdkSpan.attributes[SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES]); const outputMessages = JSON.parse( - aiSdkSpan.attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] as string, + aiSdkSpan.attributes[ + SpanAttributes.ATTR_GEN_AI_OUTPUT_MESSAGES + ] as string, ); assert.ok(Array.isArray(outputMessages)); assert.strictEqual(outputMessages.length, 1); @@ -317,14 +325,16 @@ describe("Test AI SDK Integration with Recording", function () { ); assert.strictEqual( - generateTextSpan.attributes["gen_ai.system"], + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Anthropic", ); assert.ok( - (generateTextSpan.attributes["gen_ai.request.model"] as string).includes( - "claude", - ), + ( + generateTextSpan.attributes[ + SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL + ] as string + ).includes("claude"), ); assert.ok( @@ -389,10 +399,13 @@ describe("Test AI SDK Integration with Recording", function () { "Could not find OpenAI generateText span with cache tokens", ); - assert.strictEqual(generateTextSpan.attributes["gen_ai.system"], "OpenAI"); + assert.strictEqual( + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "OpenAI", + ); assert.strictEqual( - generateTextSpan.attributes["gen_ai.request.model"], + generateTextSpan.attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o-mini", ); diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 1ed0d42d..30f60915 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -596,7 +596,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name`], + attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name`], "getWeather", ); assert.strictEqual( @@ -851,11 +851,10 @@ describe("AI SDK Transformations", () => { }); describe("transformAiSdkAttributes - prompt tokens", () => { - it("should delete ai.usage.promptTokens and gen_ai.usage.prompt_tokens (keep input_tokens)", () => { + it("should delete ai.usage.promptTokens and keep input_tokens", () => { const attributes = { "ai.usage.promptTokens": 50, "gen_ai.usage.input_tokens": 50, - "gen_ai.usage.prompt_tokens": 50, someOtherAttr: "value", }; @@ -866,10 +865,6 @@ describe("AI SDK Transformations", () => { 50, ); assert.strictEqual(attributes["ai.usage.promptTokens"], undefined); - assert.strictEqual( - attributes[SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS], - undefined, - ); assert.strictEqual(attributes.someOtherAttr, "value"); }); @@ -902,11 +897,10 @@ describe("AI SDK Transformations", () => { }); describe("transformAiSdkAttributes - completion tokens", () => { - it("should delete ai.usage.completionTokens and gen_ai.usage.completion_tokens (keep output_tokens)", () => { + it("should delete ai.usage.completionTokens and keep output_tokens", () => { const attributes = { "ai.usage.completionTokens": 25, "gen_ai.usage.output_tokens": 25, - "gen_ai.usage.completion_tokens": 25, someOtherAttr: "value", }; @@ -917,10 +911,6 @@ describe("AI SDK Transformations", () => { 25, ); assert.strictEqual(attributes["ai.usage.completionTokens"], undefined); - assert.strictEqual( - attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS], - undefined, - ); assert.strictEqual(attributes.someOtherAttr, "value"); }); @@ -1024,6 +1014,10 @@ describe("AI SDK Transformations", () => { assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "openai", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); assert.strictEqual(attributes["ai.model.provider"], undefined); @@ -1046,6 +1040,10 @@ describe("AI SDK Transformations", () => { assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "openai", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); assert.strictEqual(attributes["ai.model.provider"], undefined); @@ -1064,6 +1062,10 @@ describe("AI SDK Transformations", () => { assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "azure-openai", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Azure", ); assert.strictEqual(attributes["ai.model.provider"], undefined); @@ -1079,6 +1081,10 @@ describe("AI SDK Transformations", () => { assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "anthropic", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Anthropic", ); assert.strictEqual(attributes["ai.model.provider"], undefined); @@ -1102,9 +1108,10 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); + // Empty provider should not set ATTR_GEN_AI_PROVIDER_NAME assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], - "", + undefined, ); assert.strictEqual(attributes["ai.model.provider"], undefined); }); @@ -1159,6 +1166,10 @@ describe("AI SDK Transformations", () => { // Check vendor transformation assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "openai", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); @@ -1244,6 +1255,10 @@ describe("AI SDK Transformations", () => { // Check vendor transformation assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "azure-openai", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Azure", ); @@ -1291,15 +1306,15 @@ describe("AI SDK Transformations", () => { // Check tools transformation assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name`], + attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name`], "getWeather", ); assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.description`], + attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.description`], "Get weather for a location", ); assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.parameters`], + attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.parameters`], JSON.stringify({ type: "object", properties: { location: { type: "string" } }, @@ -1573,11 +1588,11 @@ describe("AI SDK Transformations", () => { // Check that tools are also properly transformed assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.0.name`], + attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.0.name`], "getWeather", ); assert.strictEqual( - attributes[`${SpanAttributes.ATTR_GEN_AI_TOOL_NAME}.1.name`], + attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.1.name`], "searchRestaurants", ); }); @@ -1861,6 +1876,10 @@ describe("AI SDK Transformations", () => { assert.strictEqual(attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS], 15); assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + "openai", + ); + assert.strictEqual( + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); @@ -2120,7 +2139,7 @@ describe("AI SDK Transformations", () => { "openai", ); assert.strictEqual( - attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "OpenAI", ); }); @@ -2137,7 +2156,7 @@ describe("AI SDK Transformations", () => { "azure-openai", ); assert.strictEqual( - attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Azure", ); }); @@ -2154,7 +2173,7 @@ describe("AI SDK Transformations", () => { "anthropic", ); assert.strictEqual( - attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], "Anthropic", ); }); @@ -2437,7 +2456,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.LLM_RESPONSE_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], "gpt-4o", ); assert.strictEqual( @@ -2456,7 +2475,7 @@ describe("AI SDK Transformations", () => { transformLLMSpans(attributes); assert.strictEqual( - attributes[SpanAttributes.LLM_RESPONSE_MODEL], + attributes[SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL], undefined, ); assert.strictEqual( @@ -2505,11 +2524,11 @@ describe("AI SDK Transformations", () => { // Check provider transformations assert.strictEqual( attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], - "OpenAI", + "openai", ); assert.strictEqual( - attributes[SpanAttributes.ATTR_GEN_AI_PROVIDER_NAME], - "openai", + attributes[SpanAttributes.ATTR_GEN_AI_SYSTEM], + "OpenAI", ); // Check response transformations diff --git a/packages/traceloop-sdk/test/decorators.test.ts b/packages/traceloop-sdk/test/decorators.test.ts index 97fef441..74b0cb1c 100644 --- a/packages/traceloop-sdk/test/decorators.test.ts +++ b/packages/traceloop-sdk/test/decorators.test.ts @@ -149,11 +149,11 @@ describe("Test SDK Decorators", () => { "sample_chat", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); }); @@ -264,11 +264,11 @@ describe("Test SDK Decorators", () => { "sample_chat", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); }); @@ -353,11 +353,11 @@ describe("Test SDK Decorators", () => { workflowName, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], "Tell me a joke about OpenTelemetry", ); }); @@ -414,15 +414,15 @@ describe("Test SDK Decorators", () => { "sample_chat", ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], undefined, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`], undefined, ); assert.strictEqual( - chatSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + chatSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`], undefined, ); }); @@ -490,35 +490,41 @@ describe("Test SDK Decorators", () => { "chat", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL}`], "gpt-3.5-turbo", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL}`], "gpt-3.5-turbo-0125", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + completionSpan.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`], "user", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "Tell me a joke about OpenTelemetry", ); assert.strictEqual( - completionSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` + ], result.choices[0].message.content, ); assert.ok( completionSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`], ); assert.equal( - completionSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`], + completionSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_PROMPT_TOKENS}` + ], "15", ); assert.ok( +completionSpan.attributes[ - `${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}` + `${SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS}` ]! > 0, ); }); @@ -553,12 +559,12 @@ describe("Test SDK Decorators", () => { const openAI1Span = spans.find( (span) => - span.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] === + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === "Tell me a joke about OpenTelemetry", ); const openAI2Span = spans.find( (span) => - span.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] === + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === "Tell me a joke about Typescript", ); @@ -653,37 +659,51 @@ describe("Test SDK Decorators", () => { assert.ok(result); assert.ok(generateTextSpan); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role` + ], "user", ); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content` + ], "What is the capital of France?", ); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL}` + ], "gpt-3.5-turbo", ); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL}` + ], "gpt-3.5-turbo-0125", ); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role` + ], "assistant", ); assert.strictEqual( generateTextSpan.attributes[ - `${SpanAttributes.LLM_COMPLETIONS}.0.content` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content` ], result.text, ); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_USAGE_INPUT_TOKENS}`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_INPUT_TOKENS}` + ], 14, ); assert.strictEqual( - generateTextSpan.attributes[`${SpanAttributes.LLM_USAGE_OUTPUT_TOKENS}`], + generateTextSpan.attributes[ + `${SpanAttributes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS}` + ], 8, ); assert.strictEqual( From d75e0c385cfc7095e23a09e33cd323a5fe850420 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:52:58 +0200 Subject: [PATCH 11/24] fix test --- .../instrumentation-together/test/instrumentation.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/instrumentation-together/test/instrumentation.test.ts b/packages/instrumentation-together/test/instrumentation.test.ts index a14f05f2..c69c0bea 100644 --- a/packages/instrumentation-together/test/instrumentation.test.ts +++ b/packages/instrumentation-together/test/instrumentation.test.ts @@ -590,28 +590,28 @@ describe("Test Together instrumentation", async function () { assert.ok(completionSpan); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.name` ], "get_current_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.0.arguments` ]! as string, ), { location: "Boston, MA", unit: "fahrenheit" }, ); assert.strictEqual( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.name` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.name` ], "get_tomorrow_weather", ); assert.deepEqual( JSON.parse( completionSpan.attributes[ - `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.function_call.arguments` + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.tool_calls.1.arguments` ]! as string, ), { location: "Chicago, IL", unit: "fahrenheit" }, From 7f9e86c79e9b89cb322de32d7bc71125d05d8f24 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:54:25 +0200 Subject: [PATCH 12/24] pretty --- .../src/instrumentation.ts | 22 ++++++++---- .../src/instrumentation.ts | 34 +++++++++++++------ .../src/instrumentation.ts | 17 ++++++---- .../src/custom-llm-instrumentation.ts | 15 ++++++-- .../src/image-wrappers.ts | 31 ++++++++++------- .../src/instrumentation.ts | 21 ++++++++---- .../src/instrumentation.ts | 21 ++++++++---- .../src/aiplatform-instrumentation.ts | 5 ++- .../src/vertexai-instrumentation.ts | 3 +- 9 files changed, 113 insertions(+), 56 deletions(-) diff --git a/packages/instrumentation-anthropic/src/instrumentation.ts b/packages/instrumentation-anthropic/src/instrumentation.ts index 01fa2302..7ddee9fb 100644 --- a/packages/instrumentation-anthropic/src/instrumentation.ts +++ b/packages/instrumentation-anthropic/src/instrumentation.ts @@ -210,7 +210,8 @@ export class AnthropicInstrumentation extends InstrumentationBase { try { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = + params.temperature; attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p; attributes[SpanAttributes.LLM_TOP_K] = params.top_k; @@ -226,7 +227,8 @@ export class AnthropicInstrumentation extends InstrumentationBase { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens_to_sample; } else { - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = + params.max_tokens; } if ( @@ -244,7 +246,8 @@ export class AnthropicInstrumentation extends InstrumentationBase { // If a system prompt is provided, it should always be first if ("system" in params && params.system !== undefined) { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "system"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = + "system"; attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = typeof params.system === "string" ? params.system @@ -254,8 +257,9 @@ export class AnthropicInstrumentation extends InstrumentationBase { params.messages.forEach((message, index) => { const currentIndex = index + promptIndex; - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${currentIndex}.role`] = - message.role; + attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${currentIndex}.role` + ] = message.role; if (typeof message.content === "string") { attributes[ `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${currentIndex}.content` @@ -268,7 +272,8 @@ export class AnthropicInstrumentation extends InstrumentationBase { }); } else { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = + params.prompt; } } } catch (e) { @@ -477,7 +482,10 @@ export class AnthropicInstrumentation extends InstrumentationBase { result: Completion; }) { try { - span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result.model); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, + result.model, + ); if (type === "chat" && result.usage) { span.setAttribute( SpanAttributes.LLM_USAGE_TOTAL_TOKENS, diff --git a/packages/instrumentation-bedrock/src/instrumentation.ts b/packages/instrumentation-bedrock/src/instrumentation.ts index 8466dd69..4b91b0c1 100644 --- a/packages/instrumentation-bedrock/src/instrumentation.ts +++ b/packages/instrumentation-bedrock/src/instrumentation.ts @@ -298,8 +298,10 @@ export class BedrockInstrumentation extends InstrumentationBase { case "ai21": { return { [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["topP"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["maxTokens"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: + requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: + requestBody["maxTokens"], [SpanAttributes.LLM_PRESENCE_PENALTY]: requestBody["presencePenalty"]["scale"], [SpanAttributes.LLM_FREQUENCY_PENALTY]: @@ -338,7 +340,8 @@ export class BedrockInstrumentation extends InstrumentationBase { const baseAttributes = { [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["top_p"], [SpanAttributes.LLM_TOP_K]: requestBody["top_k"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: + requestBody["temperature"], [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["max_tokens_to_sample"] || requestBody["max_tokens"], }; @@ -351,9 +354,12 @@ export class BedrockInstrumentation extends InstrumentationBase { if (requestBody["messages"]) { const promptAttributes: Record = {}; requestBody["messages"].forEach((message: any, index: number) => { - promptAttributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = - message.role; - promptAttributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = + promptAttributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role` + ] = message.role; + promptAttributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content` + ] = typeof message.content === "string" ? message.content : JSON.stringify(message.content); @@ -366,7 +372,9 @@ export class BedrockInstrumentation extends InstrumentationBase { return { ...baseAttributes, [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`]: "user", - [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody["prompt"] + [`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`]: requestBody[ + "prompt" + ] // The format is removing when we are setting span attribute .replace("\n\nHuman:", "") .replace("\n\nAssistant:", ""), @@ -379,8 +387,10 @@ export class BedrockInstrumentation extends InstrumentationBase { return { [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["p"], [SpanAttributes.LLM_TOP_K]: requestBody["k"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["max_tokens"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: + requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: + requestBody["max_tokens"], // Prompt & Role ...(this._shouldSendPrompts() @@ -395,8 +405,10 @@ export class BedrockInstrumentation extends InstrumentationBase { case "meta": { return { [SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P]: requestBody["top_p"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: requestBody["temperature"], - [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: requestBody["max_gen_len"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE]: + requestBody["temperature"], + [SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS]: + requestBody["max_gen_len"], // Prompt & Role ...(this._shouldSendPrompts() diff --git a/packages/instrumentation-cohere/src/instrumentation.ts b/packages/instrumentation-cohere/src/instrumentation.ts index 43cc1199..8b1799d5 100644 --- a/packages/instrumentation-cohere/src/instrumentation.ts +++ b/packages/instrumentation-cohere/src/instrumentation.ts @@ -228,12 +228,14 @@ export class CohereInstrumentation extends InstrumentationBase { if (!("query" in params)) { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.p; attributes[SpanAttributes.LLM_TOP_K] = params.k; - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = + params.temperature; attributes[SpanAttributes.LLM_FREQUENCY_PENALTY] = params.frequencyPenalty; attributes[SpanAttributes.LLM_PRESENCE_PENALTY] = params.presencePenalty; - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.maxTokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = + params.maxTokens; } else { attributes["topN"] = params["topN"]; attributes["maxChunksPerDoc"] = params["maxChunksPerDoc"]; @@ -242,14 +244,16 @@ export class CohereInstrumentation extends InstrumentationBase { if (this._shouldSendPrompts()) { if (type === "completion" && "prompt" in params) { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`] = + params.prompt; } else if (type === "chat" && "message" in params) { params.chatHistory?.forEach((msg, index) => { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = msg.role; if (msg.role !== "TOOL") { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = - msg.message; + attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content` + ] = msg.message; } }); @@ -261,7 +265,8 @@ export class CohereInstrumentation extends InstrumentationBase { ] = params.message; } else if (type === "rerank" && "query" in params) { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`] = params.query; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.user`] = + params.query; params.documents.forEach((doc, index) => { attributes[`documents.${index}.index`] = typeof doc === "string" ? doc : doc.text; diff --git a/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts b/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts index 3479a5f7..3eac2e94 100644 --- a/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts +++ b/packages/instrumentation-llamaindex/src/custom-llm-instrumentation.ts @@ -134,7 +134,10 @@ export class CustomLLMInstrumentation { span: Span, metadata: llamaindex.LLMMetadata, ): T { - span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, metadata.model); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, + metadata.model, + ); if (!shouldSendPrompts(this.config)) { span.setStatus({ code: SpanStatusCode.OK }); @@ -178,7 +181,10 @@ export class CustomLLMInstrumentation { execContext: Context, metadata: llamaindex.LLMMetadata, ): T { - span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, metadata.model); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, + metadata.model, + ); if (!shouldSendPrompts(this.config)) { span.setStatus({ code: SpanStatusCode.OK }); span.end(); @@ -186,7 +192,10 @@ export class CustomLLMInstrumentation { } return llmGeneratorWrapper(result, execContext, (message) => { - span.setAttribute(`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, message); + span.setAttribute( + `${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`, + message, + ); span.setStatus({ code: SpanStatusCode.OK }); span.end(); }) as any; diff --git a/packages/instrumentation-openai/src/image-wrappers.ts b/packages/instrumentation-openai/src/image-wrappers.ts index b3b83aca..77f8dad8 100644 --- a/packages/instrumentation-openai/src/image-wrappers.ts +++ b/packages/instrumentation-openai/src/image-wrappers.ts @@ -161,7 +161,8 @@ export function setImageGenerationRequestAttributes( } if (params.prompt) { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = + params.prompt; attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; } @@ -192,7 +193,8 @@ export async function setImageEditRequestAttributes( } if (params.prompt) { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = + params.prompt; attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; } @@ -215,9 +217,8 @@ export async function setImageEditRequestAttributes( ); if (imageUrl) { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.content`] = JSON.stringify([ - { type: "image_url", image_url: { url: imageUrl } }, - ]); + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.content`] = + JSON.stringify([{ type: "image_url", image_url: { url: imageUrl } }]); attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.1.role`] = "user"; } } @@ -267,9 +268,8 @@ export async function setImageVariationRequestAttributes( ); if (imageUrl) { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify([ - { type: "image_url", image_url: { url: imageUrl } }, - ]); + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] = + JSON.stringify([{ type: "image_url", image_url: { url: imageUrl } }]); attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.role`] = "user"; } } @@ -295,7 +295,8 @@ export async function setImageGenerationResponseAttributes( params, response.data.length, ); - attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS] = completionTokens; + attributes[SpanAttributes.ATTR_GEN_AI_USAGE_COMPLETION_TOKENS] = + completionTokens; // Calculate prompt tokens if enrichTokens is enabled if (instrumentationConfig?.enrichTokens) { @@ -342,7 +343,8 @@ export async function setImageGenerationResponseAttributes( attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([{ type: "image_url", image_url: { url: imageUrl } }]); - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + "assistant"; } catch (error) { console.error("Failed to upload generated image:", error); } @@ -367,21 +369,24 @@ export async function setImageGenerationResponseAttributes( JSON.stringify([ { type: "image_url", image_url: { url: uploadedUrl } }, ]); - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + "assistant"; } catch (error) { console.error("Failed to fetch and upload generated image:", error); attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([ { type: "image_url", image_url: { url: firstImage.url } }, ]); - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + "assistant"; } } else if (firstImage.url) { attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([ { type: "image_url", image_url: { url: firstImage.url } }, ]); - attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant"; + attributes[`${SpanAttributes.ATTR_GEN_AI_COMPLETION}.0.role`] = + "assistant"; } if (firstImage.revised_prompt) { diff --git a/packages/instrumentation-openai/src/instrumentation.ts b/packages/instrumentation-openai/src/instrumentation.ts index 06965bd0..a1cf4c54 100644 --- a/packages/instrumentation-openai/src/instrumentation.ts +++ b/packages/instrumentation-openai/src/instrumentation.ts @@ -305,10 +305,12 @@ export class OpenAIInstrumentation extends InstrumentationBase { try { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; if (params.max_tokens) { - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = + params.max_tokens; } if (params.temperature) { - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = + params.temperature; } if (params.top_p) { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p; @@ -338,11 +340,13 @@ export class OpenAIInstrumentation extends InstrumentationBase { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = message.role; if (typeof message.content === "string") { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = - (message.content as string) || ""; + attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content` + ] = (message.content as string) || ""; } else { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = - JSON.stringify(message.content); + attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content` + ] = JSON.stringify(message.content); } }); params.functions?.forEach((func, index) => { @@ -653,7 +657,10 @@ export class OpenAIInstrumentation extends InstrumentationBase { | { span: Span; type: "chat"; result: ChatCompletion } | { span: Span; type: "completion"; result: Completion }) { try { - span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result.model); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, + result.model, + ); if (result.usage) { span.setAttribute( SpanAttributes.LLM_USAGE_TOTAL_TOKENS, diff --git a/packages/instrumentation-together/src/instrumentation.ts b/packages/instrumentation-together/src/instrumentation.ts index 3063b430..c3caabe9 100644 --- a/packages/instrumentation-together/src/instrumentation.ts +++ b/packages/instrumentation-together/src/instrumentation.ts @@ -194,10 +194,12 @@ export class TogetherInstrumentation extends InstrumentationBase { try { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = params.model; if (params.max_tokens) { - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MAX_TOKENS] = + params.max_tokens; } if (params.temperature) { - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature; + attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TEMPERATURE] = + params.temperature; } if (params.top_p) { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p; @@ -226,11 +228,13 @@ export class TogetherInstrumentation extends InstrumentationBase { attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.role`] = message.role; if (typeof message.content === "string") { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = - (message.content as string) || ""; + attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content` + ] = (message.content as string) || ""; } else { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content`] = - JSON.stringify(message.content); + attributes[ + `${SpanAttributes.ATTR_GEN_AI_PROMPT}.${index}.content` + ] = JSON.stringify(message.content); } }); @@ -495,7 +499,10 @@ export class TogetherInstrumentation extends InstrumentationBase { | { span: Span; type: "chat"; result: ChatCompletion } | { span: Span; type: "completion"; result: Completion }) { try { - span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result.model); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, + result.model, + ); if (result.usage) { span.setAttribute( diff --git a/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts b/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts index bc4f701c..04796668 100644 --- a/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts +++ b/packages/instrumentation-vertexai/src/aiplatform-instrumentation.ts @@ -271,7 +271,10 @@ export class AIPlatformInstrumentation extends InstrumentationBase { }) { try { if (result[0].model) - span.setAttribute(SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, result[0].model); + span.setAttribute( + SpanAttributes.ATTR_GEN_AI_RESPONSE_MODEL, + result[0].model, + ); if (result) { if (result[0].metadata) { diff --git a/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts b/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts index bab5bd20..12c9ce94 100644 --- a/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts +++ b/packages/instrumentation-vertexai/src/vertexai-instrumentation.ts @@ -160,7 +160,8 @@ export class VertexAIInstrumentation extends InstrumentationBase { let i = 0; if (instance["systemInstruction"]) { - attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i}.role`] = "system"; + attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i}.role`] = + "system"; attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.${i}.content`] = this._formatPartsData(instance["systemInstruction"].parts); From a0b2f9d0f8f49fa759ea9e12feed2fb7b38d8c46 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:14:11 +0200 Subject: [PATCH 13/24] wip --- converted-old.json | 34 +++++++++++++++++++ converted.json | 18 ++++++++++ .../src/instrumentation.ts | 1 - .../src/sample_chatbot_interactive.ts | 24 +++++++------ 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 converted-old.json create mode 100644 converted.json diff --git a/converted-old.json b/converted-old.json new file mode 100644 index 00000000..c29e213e --- /dev/null +++ b/converted-old.json @@ -0,0 +1,34 @@ +{ + "llm.request.functions.0.name": "getWeather", + "llm.usage.input_tokens": "198", + "llm.request.functions.0.description": "Get the current weather for a specified location", + "llm.usage.total_tokens": "266", + "llm.usage.output_tokens": "68", + "ai.model.id": "gpt-4o", + "llm.request.functions.1.name": "calculateDistance", + "ai.settings.maxRetries": "2", + "ai.operationId": "ai.generateText.doGenerate", + "llm.response.model": "gpt-4o-2024-08-06", + "llm.request.functions.1.parameters": "{\"type\":\"object\",\"properties\":{\"fromCity\":{\"type\":\"string\",\"description\":\"The starting city\"},\"toCity\":{\"type\":\"string\",\"description\":\"The destination city\"}},\"required\":[\"fromCity\",\"toCity\"],\"additionalProperties\":false,\"$schema\":\"http:\/\/json-schema.org\/draft-07\/schema#\"}", + "llm.vendor": "OpenAI", + "llm.request.functions.2.description": "Search for restaurants in a specific city", + "llm.request.functions.2.parameters": "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"description\":\"The city to search for restaurants\"},\"cuisine\":{\"type\":\"string\",\"description\":\"Optional cuisine type (e.g., Italian, Mexican)\"}},\"required\":[\"city\"],\"additionalProperties\":false,\"$schema\":\"http:\/\/json-schema.org\/draft-07\/schema#\"}", + "llm.input.messages": "[{\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"content\":\"Help me plan a trip to San Francisco. I'd like to know:\\n1. What's the weather like there?\\n2. Find some good restaurants to try\\n3. If I'm traveling from New York, how far is it?\\n\\nPlease use the available tools to get current information and provide a comprehensive travel guide.\"}]}]", + "llm.request.model": "gpt-4o", + "operation.name": "ai.generateText.doGenerate", + "llm.response.id": "chatcmpl-Cl94uy98kpuOz4HHT3zHlCinM11XY", + "llm.output.messages": "[{\"role\":\"assistant\",\"parts\":[{\"type\":\"tool_call\",\"tool_call\":{\"name\":\"getWeather\",\"arguments\":\"{\\\"location\\\": \\\"San Francisco\\\"}\"}},{\"type\":\"tool_call\",\"tool_call\":{\"name\":\"searchRestaurants\",\"arguments\":\"{\\\"city\\\": \\\"San Francisco\\\"}\"}},{\"type\":\"tool_call\",\"tool_call\":{\"name\":\"calculateDistance\",\"arguments\":\"{\\\"fromCity\\\": \\\"New York\\\", \\\"toCity\\\": \\\"San Francisco\\\"}\"}}]}]", + "ai.response.model": "gpt-4o-2024-08-06", + "llm.usage.cache_read_input_tokens": "0", + "ai.prompt.toolChoice": "{\"type\":\"auto\"}", + "ai.response.finishReason": "tool-calls", + "llm.usage.reasoning_tokens": "0", + "llm.request.functions.2.name": "searchRestaurants", + "ai.prompt.format": "prompt", + "ai.response.timestamp": "2025-12-10T07:39:08.000Z", + "llm.request.functions.0.parameters": "{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"The location to get the weather for\"}},\"required\":[\"location\"],\"additionalProperties\":false,\"$schema\":\"http:\/\/json-schema.org\/draft-07\/schema#\"}", + "traceloop.workflow.name": "plan_trip", + "ai.response.id": "chatcmpl-Cl94uy98kpuOz4HHT3zHlCinM11XY", + "llm.agent.name": "plan_trip", + "llm.request.functions.1.description": "Calculate the distance between two cities" +} diff --git a/converted.json b/converted.json new file mode 100644 index 00000000..46679f8d --- /dev/null +++ b/converted.json @@ -0,0 +1,18 @@ +{ + "llm.usage.reasoning_tokens": "0", + "llm.usage.output_tokens": "7", + "ai.settings.maxSteps": "1", + "llm.usage.cache_read_input_tokens": "0", + "ai.operationId": "ai.generateText", + "llm.output.messages": "[{\"role\":\"assistant\",\"parts\":[{\"type\":\"text\",\"content\":\"The capital of France is Paris.\"}]}]", + "llm.vendor": "OpenAI", + "llm.operation.name": "chat", + "llm.request.model": "gpt-3.5-turbo", + "llm.usage.total_tokens": "21", + "ai.prompt": "{\"messages\":[{\"role\":\"user\",\"content\":\"What is the capital of France?\"}]}", + "ai.settings.maxRetries": "2", + "operation.name": "ai.generateText", + "llm.usage.input_tokens": "14", + "traceloop.workflow.name": "chat", + "llm.provider.name": "openai" +} diff --git a/packages/instrumentation-cohere/src/instrumentation.ts b/packages/instrumentation-cohere/src/instrumentation.ts index 8b1799d5..9f4165c7 100644 --- a/packages/instrumentation-cohere/src/instrumentation.ts +++ b/packages/instrumentation-cohere/src/instrumentation.ts @@ -223,7 +223,6 @@ export class CohereInstrumentation extends InstrumentationBase { try { const model = params.model ?? "command"; attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = model; - attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_MODEL] = model; if (!("query" in params)) { attributes[SpanAttributes.ATTR_GEN_AI_REQUEST_TOP_P] = params.p; diff --git a/packages/sample-app/src/sample_chatbot_interactive.ts b/packages/sample-app/src/sample_chatbot_interactive.ts index d3c56b4e..e932580b 100644 --- a/packages/sample-app/src/sample_chatbot_interactive.ts +++ b/packages/sample-app/src/sample_chatbot_interactive.ts @@ -78,9 +78,7 @@ class InteractiveChatbot { content: userMessage, }); - console.log( - `\n${colors.green}${colors.bright}Assistant: ${colors.reset}`, - ); + console.log(`\n${colors.green}${colors.bright}Assistant: ${colors.reset}`); // Stream the response const result = await streamText({ @@ -124,10 +122,18 @@ class InteractiveChatbot { } async start(): Promise { - console.log(`${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗`); - console.log(`║ Interactive AI Chatbot with Traceloop ║`); - console.log(`╚════════════════════════════════════════════════════════════╝${colors.reset}\n`); - console.log(`${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`); + console.log( + `${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗`, + ); + console.log( + `║ Interactive AI Chatbot with Traceloop ║`, + ); + console.log( + `╚════════════════════════════════════════════════════════════╝${colors.reset}\n`, + ); + console.log( + `${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`, + ); this.rl.prompt(); @@ -140,9 +146,7 @@ class InteractiveChatbot { } if (trimmedInput === "/exit") { - console.log( - `\n${colors.magenta}Goodbye! 👋${colors.reset}\n`, - ); + console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`); this.rl.close(); process.exit(0); } From 34a5be2a8bca58cd7a138f73328cb5b4add8e5b2 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:48:51 +0200 Subject: [PATCH 14/24] added associations --- converted-old.json | 34 -- converted.json | 18 -- packages/sample-app/package.json | 1 + .../sample-app/src/sample_associations.ts | 167 ++++++++++ packages/traceloop-sdk/package.json | 1 + .../traceloop-sdk/src/lib/node-server-sdk.ts | 1 + .../src/lib/tracing/ai-sdk-transformations.ts | 6 +- .../src/lib/tracing/associations.ts | 80 +++++ .../src/lib/tracing/span-processor.ts | 5 +- .../traceloop-sdk/test/associations.test.ts | 297 ++++++++++++++++++ pnpm-lock.yaml | 11 +- 11 files changed, 557 insertions(+), 64 deletions(-) delete mode 100644 converted-old.json delete mode 100644 converted.json create mode 100644 packages/sample-app/src/sample_associations.ts create mode 100644 packages/traceloop-sdk/src/lib/tracing/associations.ts create mode 100644 packages/traceloop-sdk/test/associations.test.ts diff --git a/converted-old.json b/converted-old.json deleted file mode 100644 index c29e213e..00000000 --- a/converted-old.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "llm.request.functions.0.name": "getWeather", - "llm.usage.input_tokens": "198", - "llm.request.functions.0.description": "Get the current weather for a specified location", - "llm.usage.total_tokens": "266", - "llm.usage.output_tokens": "68", - "ai.model.id": "gpt-4o", - "llm.request.functions.1.name": "calculateDistance", - "ai.settings.maxRetries": "2", - "ai.operationId": "ai.generateText.doGenerate", - "llm.response.model": "gpt-4o-2024-08-06", - "llm.request.functions.1.parameters": "{\"type\":\"object\",\"properties\":{\"fromCity\":{\"type\":\"string\",\"description\":\"The starting city\"},\"toCity\":{\"type\":\"string\",\"description\":\"The destination city\"}},\"required\":[\"fromCity\",\"toCity\"],\"additionalProperties\":false,\"$schema\":\"http:\/\/json-schema.org\/draft-07\/schema#\"}", - "llm.vendor": "OpenAI", - "llm.request.functions.2.description": "Search for restaurants in a specific city", - "llm.request.functions.2.parameters": "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"description\":\"The city to search for restaurants\"},\"cuisine\":{\"type\":\"string\",\"description\":\"Optional cuisine type (e.g., Italian, Mexican)\"}},\"required\":[\"city\"],\"additionalProperties\":false,\"$schema\":\"http:\/\/json-schema.org\/draft-07\/schema#\"}", - "llm.input.messages": "[{\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"content\":\"Help me plan a trip to San Francisco. I'd like to know:\\n1. What's the weather like there?\\n2. Find some good restaurants to try\\n3. If I'm traveling from New York, how far is it?\\n\\nPlease use the available tools to get current information and provide a comprehensive travel guide.\"}]}]", - "llm.request.model": "gpt-4o", - "operation.name": "ai.generateText.doGenerate", - "llm.response.id": "chatcmpl-Cl94uy98kpuOz4HHT3zHlCinM11XY", - "llm.output.messages": "[{\"role\":\"assistant\",\"parts\":[{\"type\":\"tool_call\",\"tool_call\":{\"name\":\"getWeather\",\"arguments\":\"{\\\"location\\\": \\\"San Francisco\\\"}\"}},{\"type\":\"tool_call\",\"tool_call\":{\"name\":\"searchRestaurants\",\"arguments\":\"{\\\"city\\\": \\\"San Francisco\\\"}\"}},{\"type\":\"tool_call\",\"tool_call\":{\"name\":\"calculateDistance\",\"arguments\":\"{\\\"fromCity\\\": \\\"New York\\\", \\\"toCity\\\": \\\"San Francisco\\\"}\"}}]}]", - "ai.response.model": "gpt-4o-2024-08-06", - "llm.usage.cache_read_input_tokens": "0", - "ai.prompt.toolChoice": "{\"type\":\"auto\"}", - "ai.response.finishReason": "tool-calls", - "llm.usage.reasoning_tokens": "0", - "llm.request.functions.2.name": "searchRestaurants", - "ai.prompt.format": "prompt", - "ai.response.timestamp": "2025-12-10T07:39:08.000Z", - "llm.request.functions.0.parameters": "{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"The location to get the weather for\"}},\"required\":[\"location\"],\"additionalProperties\":false,\"$schema\":\"http:\/\/json-schema.org\/draft-07\/schema#\"}", - "traceloop.workflow.name": "plan_trip", - "ai.response.id": "chatcmpl-Cl94uy98kpuOz4HHT3zHlCinM11XY", - "llm.agent.name": "plan_trip", - "llm.request.functions.1.description": "Calculate the distance between two cities" -} diff --git a/converted.json b/converted.json deleted file mode 100644 index 46679f8d..00000000 --- a/converted.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "llm.usage.reasoning_tokens": "0", - "llm.usage.output_tokens": "7", - "ai.settings.maxSteps": "1", - "llm.usage.cache_read_input_tokens": "0", - "ai.operationId": "ai.generateText", - "llm.output.messages": "[{\"role\":\"assistant\",\"parts\":[{\"type\":\"text\",\"content\":\"The capital of France is Paris.\"}]}]", - "llm.vendor": "OpenAI", - "llm.operation.name": "chat", - "llm.request.model": "gpt-3.5-turbo", - "llm.usage.total_tokens": "21", - "ai.prompt": "{\"messages\":[{\"role\":\"user\",\"content\":\"What is the capital of France?\"}]}", - "ai.settings.maxRetries": "2", - "operation.name": "ai.generateText", - "llm.usage.input_tokens": "14", - "traceloop.workflow.name": "chat", - "llm.provider.name": "openai" -} diff --git a/packages/sample-app/package.json b/packages/sample-app/package.json index 542417b9..cf01cb70 100644 --- a/packages/sample-app/package.json +++ b/packages/sample-app/package.json @@ -15,6 +15,7 @@ "run:gemini": "npm run build && node dist/src/vertexai/gemini.js", "run:palm2": "npm run build && node dist/src/vertexai/palm2.js", "run:decorators": "npm run build && node dist/src/sample_decorators.js", + "run:associations": "npm run build && node dist/src/sample_associations.js", "run:with": "npm run build && node dist/src/sample_with.js", "run:prompt_mgmt": "npm run build && node dist/src/sample_prompt_mgmt.js", "run:vercel": "npm run build && node dist/src/sample_vercel_ai.js", diff --git a/packages/sample-app/src/sample_associations.ts b/packages/sample-app/src/sample_associations.ts new file mode 100644 index 00000000..7815f065 --- /dev/null +++ b/packages/sample-app/src/sample_associations.ts @@ -0,0 +1,167 @@ +import * as traceloop from "@traceloop/node-server-sdk"; +import OpenAI from "openai"; + +// Initialize Traceloop +traceloop.initialize({ + appName: "associations_demo", + apiKey: process.env.TRACELOOP_API_KEY, + disableBatch: true, +}); + +const openai = new OpenAI(); + +/** + * Sample chatbot that demonstrates the Associations API. + * This example shows how to track conversations, users, and sessions + * across multiple LLM interactions. + */ +class ChatbotWithAssociations { + constructor( + private conversationId: string, + private userId: string, + private sessionId: string, + ) {} + + /** + * Process a multi-turn conversation with associations + */ + @traceloop.workflow({ name: "chatbot_conversation" }) + async handleConversation() { + console.log("\n=== Starting Chatbot Conversation ==="); + console.log(`Conversation ID: ${this.conversationId}`); + console.log(`User ID: ${this.userId}`); + console.log(`Session ID: ${this.sessionId}\n`); + + // Set associations at the beginning of the conversation + // These will be automatically attached to all spans within this context + traceloop.Associations.set([ + [traceloop.AssociationProperty.CONVERSATION_ID, this.conversationId], + [traceloop.AssociationProperty.USER_ID, this.userId], + [traceloop.AssociationProperty.SESSION_ID, this.sessionId], + ]); + + // First message + const greeting = await this.sendMessage( + "Hello! What's the weather like today?", + ); + console.log(`Bot: ${greeting}\n`); + + // Second message in the same conversation + const followup = await this.sendMessage( + "What should I wear for that weather?", + ); + console.log(`Bot: ${followup}\n`); + + // Third message + const final = await this.sendMessage("Thanks for the advice!"); + console.log(`Bot: ${final}\n`); + + return { + greeting, + followup, + final, + }; + } + + /** + * Send a single message - this is a task within the workflow + */ + @traceloop.task({ name: "send_message" }) + private async sendMessage(userMessage: string): Promise { + console.log(`User: ${userMessage}`); + + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: userMessage }], + model: "gpt-3.5-turbo", + }); + + return completion.choices[0].message.content || "No response"; + } +} + +/** + * Simulate a customer service scenario with multiple customers + */ +async function customerServiceDemo() { + return traceloop.withWorkflow( + { name: "customer_service_scenario" }, + async () => { + console.log("\n=== Customer Service Scenario ===\n"); + + // Customer 1 + traceloop.Associations.set([ + [traceloop.AssociationProperty.CUSTOMER_ID, "cust-001"], + [traceloop.AssociationProperty.USER_ID, "agent-alice"], + ]); + + const customer1Response = await openai.chat.completions.create({ + messages: [ + { + role: "user", + content: "I need help with my order #12345", + }, + ], + model: "gpt-3.5-turbo", + }); + + console.log("Customer 1 (cust-001):"); + console.log(`Response: ${customer1Response.choices[0].message.content}\n`); + + // Customer 2 - Update associations for new customer + traceloop.Associations.set([ + [traceloop.AssociationProperty.CUSTOMER_ID, "cust-002"], + [traceloop.AssociationProperty.USER_ID, "agent-bob"], + ]); + + const customer2Response = await openai.chat.completions.create({ + messages: [ + { + role: "user", + content: "How do I return an item?", + }, + ], + model: "gpt-3.5-turbo", + }); + + console.log("Customer 2 (cust-002):"); + console.log(`Response: ${customer2Response.choices[0].message.content}\n`); + }, + ); +} + +/** + * Main execution + */ +async function main() { + console.log("============================================"); + console.log("Traceloop Associations API Demo"); + console.log("============================================"); + + try { + // Example 1: Multi-turn chatbot conversation + const chatbot = new ChatbotWithAssociations( + "conv-abc-123", // conversation_id + "user-alice-456", // user_id + "session-xyz-789", // session_id + ); + + await chatbot.handleConversation(); + + // Example 2: Customer service with multiple customers + await customerServiceDemo(); + + console.log("\n=== Demo Complete ==="); + console.log( + "Check your Traceloop dashboard to see the associations attached to traces!", + ); + console.log( + "You can filter and search by conversation_id, user_id, session_id, or customer_id.", + ); + } catch (error) { + console.error("Error running demo:", error); + process.exit(1); + } +} + +// Run the demo +main(); diff --git a/packages/traceloop-sdk/package.json b/packages/traceloop-sdk/package.json index ac1f58aa..5784ce6b 100644 --- a/packages/traceloop-sdk/package.json +++ b/packages/traceloop-sdk/package.json @@ -57,6 +57,7 @@ "dependencies": { "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-proto": "^0.203.0", "@opentelemetry/instrumentation": "^0.203.0", diff --git a/packages/traceloop-sdk/src/lib/node-server-sdk.ts b/packages/traceloop-sdk/src/lib/node-server-sdk.ts index 4236a04c..997f760d 100644 --- a/packages/traceloop-sdk/src/lib/node-server-sdk.ts +++ b/packages/traceloop-sdk/src/lib/node-server-sdk.ts @@ -64,6 +64,7 @@ export { getTraceloopTracer } from "./tracing/tracing"; export * from "./tracing/decorators"; export * from "./tracing/manual"; export * from "./tracing/association"; +export * from "./tracing/associations"; export * from "./tracing/custom-metric"; export * from "./tracing/span-processor"; export * from "./prompts"; 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 f816f861..e9f7faa3 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -601,10 +601,8 @@ const transformTelemetryMetadata = ( const stringValue = typeof value === "string" ? value : String(value); metadataAttributes[metadataKey] = stringValue; - // Also set as traceloop association property attribute - attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${metadataKey}` - ] = stringValue; + // Also set as association property attribute + attributes[metadataKey] = stringValue; } } } diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts new file mode 100644 index 00000000..fdf3fd21 --- /dev/null +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -0,0 +1,80 @@ +import { trace, context as otelContext } from "@opentelemetry/api"; +import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; +import { ASSOCATION_PROPERTIES_KEY } from "./tracing"; + +/** + * Standard association properties for tracing. + */ +export enum AssociationProperty { + CONVERSATION_ID = "conversation_id", + CUSTOMER_ID = "customer_id", + USER_ID = "user_id", + SESSION_ID = "session_id", +} + +/** + * Type alias for a single association + */ +export type Association = [AssociationProperty, string]; + +/** + * Class for managing trace associations. + */ +export class Associations { + /** + * Set associations that will be added directly to all spans in the current context. + * + * @param associations - An array of [property, value] tuples + * + * @example + * // Single association + * Associations.set([[AssociationProperty.CONVERSATION_ID, "conv-123"]]); + * + * // Multiple associations + * Associations.set([ + * [AssociationProperty.USER_ID, "user-456"], + * [AssociationProperty.SESSION_ID, "session-789"] + * ]); + */ + static set(associations: Association[]): void { + // Get current associations from context or create empty object + const existingAssociations = otelContext + .active() + .getValue(ASSOCATION_PROPERTIES_KEY) as Record | undefined; + const currentAssociations: Record = existingAssociations + ? { ...existingAssociations } + : {}; + + // Update associations with new values + for (const [prop, value] of associations) { + currentAssociations[prop] = value; + } + + // Store associations in context + const newContext = otelContext + .active() + .setValue(ASSOCATION_PROPERTIES_KEY, currentAssociations); + + // Set the new context as active using the context manager + // This is the equivalent of Python's attach(set_value(...)) + const contextManager = (otelContext as any)['_getContextManager'](); + if ( + contextManager && + contextManager instanceof AsyncLocalStorageContextManager + ) { + // For AsyncLocalStorageContextManager, we need to use the internal _asyncLocalStorage + const storage = (contextManager as any)._asyncLocalStorage; + if (storage) { + storage.enterWith(newContext); + } + } + + // Also set directly on the current span + const span = trace.getSpan(otelContext.active()); + if (span && span.isRecording()) { + for (const [prop, value] of associations) { + span.setAttribute(prop, value); + } + } + } +} diff --git a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts index 025f4345..23998b5a 100644 --- a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts +++ b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts @@ -197,10 +197,7 @@ const onSpanStart = (span: Span): void => { .getValue(ASSOCATION_PROPERTIES_KEY); if (associationProperties) { for (const [key, value] of Object.entries(associationProperties)) { - span.setAttribute( - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`, - value, - ); + span.setAttribute(key, value); } } diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts new file mode 100644 index 00000000..bbfab8ff --- /dev/null +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -0,0 +1,297 @@ +/* + * Copyright Traceloop + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from "assert"; + +import type * as OpenAIModule from "openai"; + +import * as traceloop from "../src"; + +import { Polly, setupMocha as setupPolly } from "@pollyjs/core"; +import NodeHttpAdapter from "@pollyjs/adapter-node-http"; +import FetchAdapter from "@pollyjs/adapter-fetch"; +import FSPersister from "@pollyjs/persister-fs"; +import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; +import { initializeSharedTraceloop, getSharedExporter } from "./test-setup"; + +const memoryExporter = getSharedExporter(); + +Polly.register(NodeHttpAdapter); +Polly.register(FetchAdapter); +Polly.register(FSPersister); + +describe("Test Associations API", () => { + let openai: OpenAIModule.OpenAI; + + setupPolly({ + adapters: ["node-http", "fetch"], + persister: "fs", + recordIfMissing: process.env.RECORD_MODE === "NEW", + recordFailedRequests: true, + mode: process.env.RECORD_MODE === "NEW" ? "record" : "replay", + matchRequestsBy: { + headers: false, + url: { + protocol: true, + hostname: true, + pathname: true, + query: false, + }, + }, + logging: true, + }); + + before(async function () { + if (process.env.RECORD_MODE !== "NEW") { + process.env.OPENAI_API_KEY = "test"; + } + + // Use shared initialization to avoid conflicts with other test suites + initializeSharedTraceloop(); + + // Initialize OpenAI after Polly is set up + const openAIModule: typeof OpenAIModule = await import("openai"); + openai = new openAIModule.OpenAI(); + }); + + beforeEach(function () { + const { server } = this.polly as Polly; + server.any().on("beforePersist", (_req, recording) => { + recording.request.headers = recording.request.headers.filter( + ({ name }: { name: string }) => name !== "authorization", + ); + }); + }); + + afterEach(async () => { + memoryExporter.reset(); + }); + + it("should set single association on spans", async () => { + const result = await traceloop.withWorkflow( + { name: "test_single_association" }, + async () => { + // Set a single association + traceloop.Associations.set([ + [traceloop.AssociationProperty.CONVERSATION_ID, "conv-123"], + ]); + + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Tell me a joke" }], + model: "gpt-3.5-turbo", + }); + + return chatCompletion.choices[0].message.content; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const workflowSpan = spans.find( + (span) => span.name === "test_single_association.workflow", + ); + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(workflowSpan); + assert.ok(chatSpan); + + // Check that the association is set on both workflow and LLM spans + assert.strictEqual( + workflowSpan.attributes["conversation_id"], + "conv-123", + ); + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.conversation_id` + ], + "conv-123", + ); + }); + + it("should set multiple associations on spans", async () => { + const result = await traceloop.withWorkflow( + { name: "test_multiple_associations" }, + async () => { + // Set multiple associations + traceloop.Associations.set([ + [traceloop.AssociationProperty.USER_ID, "user-456"], + [traceloop.AssociationProperty.SESSION_ID, "session-789"], + ]); + + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Tell me a fact" }], + model: "gpt-3.5-turbo", + }); + + return chatCompletion.choices[0].message.content; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const workflowSpan = spans.find( + (span) => span.name === "test_multiple_associations.workflow", + ); + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(workflowSpan); + assert.ok(chatSpan); + + // Check that both associations are set + assert.strictEqual(workflowSpan.attributes["user_id"], "user-456"); + assert.strictEqual(workflowSpan.attributes["session_id"], "session-789"); + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id` + ], + "user-456", + ); + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` + ], + "session-789", + ); + }); + + it("should allow updating associations mid-workflow", async () => { + const result = await traceloop.withWorkflow( + { name: "test_update_associations" }, + async () => { + // Set initial association + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-initial"], + ]); + + const firstCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "First message" }], + model: "gpt-3.5-turbo", + }); + + // Update association + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-updated"], + ]); + + const secondCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Second message" }], + model: "gpt-3.5-turbo", + }); + + return { + first: firstCompletion.choices[0].message.content, + second: secondCompletion.choices[0].message.content, + }; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const firstChatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + "First message", + ); + const secondChatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + "Second message", + ); + + assert.ok(result); + assert.ok(firstChatSpan); + assert.ok(secondChatSpan); + + // First span should have initial value + assert.strictEqual( + firstChatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` + ], + "session-initial", + ); + + // Second span should have updated value + assert.strictEqual( + secondChatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` + ], + "session-updated", + ); + }); + + it("should work with all AssociationProperty types", async () => { + const result = await traceloop.withWorkflow( + { name: "test_all_property_types" }, + async () => { + // Set all association property types + traceloop.Associations.set([ + [traceloop.AssociationProperty.CONVERSATION_ID, "conv-abc"], + [traceloop.AssociationProperty.CUSTOMER_ID, "customer-def"], + [traceloop.AssociationProperty.USER_ID, "user-ghi"], + [traceloop.AssociationProperty.SESSION_ID, "session-jkl"], + ]); + + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Test all properties" }], + model: "gpt-3.5-turbo", + }); + + return chatCompletion.choices[0].message.content; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(chatSpan); + + // Check all property types are set + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.conversation_id` + ], + "conv-abc", + ); + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.customer_id` + ], + "customer-def", + ); + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id` + ], + "user-ghi", + ); + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` + ], + "session-jkl", + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f00ac5d3..aa06f654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,6 +822,9 @@ importers: '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 + '@opentelemetry/context-async-hooks': + specifier: ^2.0.0 + version: 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/core': specifier: ^2.0.1 version: 2.0.1(@opentelemetry/api@1.9.0) @@ -993,7 +996,7 @@ importers: version: 0.25.8 langchain: specifier: ^0.3.30 - version: 0.3.30(@langchain/aws@0.1.13(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13))))(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@4.38.3(encoding@0.1.13))(ws@8.18.3) + version: 0.3.30(@langchain/aws@0.1.13(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13))))(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@4.38.3(encoding@0.1.13)) llamaindex: specifier: ^0.11.19 version: 0.11.21(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1)(supports-color@10.0.0))(p-retry@6.2.1)(rxjs@7.8.2)(zod@3.25.76))(@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1)(supports-color@10.0.0))(p-retry@6.2.1)(rxjs@7.8.2)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod-to-json-schema@3.25.0(zod@3.25.76))(zod@3.25.76) @@ -10612,7 +10615,7 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/openai@0.6.2(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))(ws@8.18.3)': + '@langchain/openai@0.6.2(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))': dependencies: '@langchain/core': 0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)) js-tiktoken: 1.0.20 @@ -14798,10 +14801,10 @@ snapshots: kind-of@6.0.3: {} - langchain@0.3.30(@langchain/aws@0.1.13(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13))))(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@4.38.3(encoding@0.1.13))(ws@8.18.3): + langchain@0.3.30(@langchain/aws@0.1.13(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13))))(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@4.38.3(encoding@0.1.13)): dependencies: '@langchain/core': 0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)) - '@langchain/openai': 0.6.2(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13)))(ws@8.18.3) + '@langchain/openai': 0.6.2(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13))) '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.71(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))(openai@4.38.3(encoding@0.1.13))) js-tiktoken: 1.0.20 js-yaml: 4.1.0 From 49e5bb8715a0b55fbe1a2e27b937ea741e41599e Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:20:16 +0200 Subject: [PATCH 15/24] fix custom --- .../sample-app/src/sample_associations.ts | 53 +++++++++++-------- .../src/lib/tracing/ai-sdk-transformations.ts | 8 ++- .../src/lib/tracing/associations.ts | 8 +++ .../src/lib/tracing/span-processor.ts | 7 ++- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/packages/sample-app/src/sample_associations.ts b/packages/sample-app/src/sample_associations.ts index 7815f065..7f38e714 100644 --- a/packages/sample-app/src/sample_associations.ts +++ b/packages/sample-app/src/sample_associations.ts @@ -32,7 +32,7 @@ class ChatbotWithAssociations { console.log(`User ID: ${this.userId}`); console.log(`Session ID: ${this.sessionId}\n`); - // Set associations at the beginning of the conversation + // Set standard associations at the beginning of the conversation // These will be automatically attached to all spans within this context traceloop.Associations.set([ [traceloop.AssociationProperty.CONVERSATION_ID, this.conversationId], @@ -40,27 +40,34 @@ class ChatbotWithAssociations { [traceloop.AssociationProperty.SESSION_ID, this.sessionId], ]); - // First message - const greeting = await this.sendMessage( - "Hello! What's the weather like today?", + // Use withAssociationProperties to add custom properties + // Custom properties (like chat_subject) will be prefixed with traceloop.association.properties + return traceloop.withAssociationProperties( + { chat_subject: "general" }, + async () => { + // First message + const greeting = await this.sendMessage( + "Hello! What's the weather like today?", + ); + console.log(`Bot: ${greeting}\n`); + + // Second message in the same conversation + const followup = await this.sendMessage( + "What should I wear for that weather?", + ); + console.log(`Bot: ${followup}\n`); + + // Third message + const final = await this.sendMessage("Thanks for the advice!"); + console.log(`Bot: ${final}\n`); + + return { + greeting, + followup, + final, + }; + }, ); - console.log(`Bot: ${greeting}\n`); - - // Second message in the same conversation - const followup = await this.sendMessage( - "What should I wear for that weather?", - ); - console.log(`Bot: ${followup}\n`); - - // Third message - const final = await this.sendMessage("Thanks for the advice!"); - console.log(`Bot: ${final}\n`); - - return { - greeting, - followup, - final, - }; } /** @@ -138,7 +145,7 @@ async function main() { console.log("============================================"); try { - // Example 1: Multi-turn chatbot conversation + // Example 1: Multi-turn chatbot conversation with custom properties const chatbot = new ChatbotWithAssociations( "conv-abc-123", // conversation_id "user-alice-456", // user_id @@ -155,7 +162,7 @@ async function main() { "Check your Traceloop dashboard to see the associations attached to traces!", ); console.log( - "You can filter and search by conversation_id, user_id, session_id, or customer_id.", + "You can filter and search by conversation_id, user_id, session_id, customer_id, or custom properties like chat_subject.", ); } catch (error) { console.error("Error running demo:", error); 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 e9f7faa3..8d8ffe30 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -3,6 +3,7 @@ import { SpanAttributes, TraceloopSpanKindValues, } from "@traceloop/ai-semantic-conventions"; +import { STANDARD_ASSOCIATION_PROPERTIES } from "./associations"; const AI_GENERATE_TEXT = "ai.generateText"; const AI_STREAM_TEXT = "ai.streamText"; @@ -601,8 +602,11 @@ const transformTelemetryMetadata = ( const stringValue = typeof value === "string" ? value : String(value); metadataAttributes[metadataKey] = stringValue; - // Also set as association property attribute - attributes[metadataKey] = stringValue; + // Standard properties are set directly, custom properties get prefix + const attributeKey = STANDARD_ASSOCIATION_PROPERTIES.has(metadataKey) + ? metadataKey + : `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${metadataKey}`; + attributes[attributeKey] = stringValue; } } } diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts index fdf3fd21..ae2ff655 100644 --- a/packages/traceloop-sdk/src/lib/tracing/associations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -12,6 +12,14 @@ export enum AssociationProperty { SESSION_ID = "session_id", } +/** + * Set of standard association property keys (without prefix). + * Use this to check if a property should be set directly or with the TRACELOOP_ASSOCIATION_PROPERTIES prefix. + */ +export const STANDARD_ASSOCIATION_PROPERTIES = new Set( + Object.values(AssociationProperty) +); + /** * Type alias for a single association */ diff --git a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts index 23998b5a..2756b833 100644 --- a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts +++ b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts @@ -20,6 +20,7 @@ import { transformAiSdkSpanNames, } from "./ai-sdk-transformations"; import { parseKeyPairsIntoRecord } from "./baggage-utils"; +import { STANDARD_ASSOCIATION_PROPERTIES } from "./associations"; export const ALL_INSTRUMENTATION_LIBRARIES = "all" as const; type AllInstrumentationLibraries = typeof ALL_INSTRUMENTATION_LIBRARIES; @@ -197,7 +198,11 @@ const onSpanStart = (span: Span): void => { .getValue(ASSOCATION_PROPERTIES_KEY); if (associationProperties) { for (const [key, value] of Object.entries(associationProperties)) { - span.setAttribute(key, value); + // Standard properties are set directly, custom properties get prefix + const attributeKey = STANDARD_ASSOCIATION_PROPERTIES.has(key) + ? key + : `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`; + span.setAttribute(attributeKey, value); } } From cbed775de7b8478d4561b9de86ac7b9f368bae7d Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:39:39 +0200 Subject: [PATCH 16/24] added to all --- packages/traceloop-sdk/src/lib/tracing/association.ts | 10 +++++++++- packages/traceloop-sdk/src/lib/tracing/associations.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/association.ts b/packages/traceloop-sdk/src/lib/tracing/association.ts index 07f6c850..44d71b5e 100644 --- a/packages/traceloop-sdk/src/lib/tracing/association.ts +++ b/packages/traceloop-sdk/src/lib/tracing/association.ts @@ -14,8 +14,16 @@ export function withAssociationProperties< return fn.apply(thisArg, args); } + // Get existing associations from context and merge with new properties + const existingAssociations = context + .active() + .getValue(ASSOCATION_PROPERTIES_KEY) as Record | undefined; + const mergedAssociations = existingAssociations + ? { ...existingAssociations, ...properties } + : properties; + const newContext = context .active() - .setValue(ASSOCATION_PROPERTIES_KEY, properties); + .setValue(ASSOCATION_PROPERTIES_KEY, mergedAssociations); return context.with(newContext, fn, thisArg, ...args); } diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts index ae2ff655..ea0f41a8 100644 --- a/packages/traceloop-sdk/src/lib/tracing/associations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -77,8 +77,8 @@ export class Associations { } } - // Also set directly on the current span - const span = trace.getSpan(otelContext.active()); + // Also set directly on the current span (use newContext after enterWith) + const span = trace.getSpan(newContext); if (span && span.isRecording()) { for (const [prop, value] of associations) { span.setAttribute(prop, value); From 39a1105c267911f20d72f81184578eb40a8abcc7 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:41:14 +0200 Subject: [PATCH 17/24] add test --- .../traceloop-sdk/test/associations.test.ts | 123 +++++++++++++++--- 1 file changed, 106 insertions(+), 17 deletions(-) diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts index bbfab8ff..07315bfd 100644 --- a/packages/traceloop-sdk/test/associations.test.ts +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -268,30 +268,119 @@ describe("Test Associations API", () => { assert.ok(result); assert.ok(chatSpan); - // Check all property types are set - assert.strictEqual( - chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.conversation_id` - ], - "conv-abc", + // Check all property types are set (standard properties without prefix) + assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-abc"); + assert.strictEqual(chatSpan.attributes["customer_id"], "customer-def"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-ghi"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-jkl"); + }); + + it("should propagate associations to all child spans", async () => { + @traceloop.task({ name: "subtask" }) + async function subtask() { + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Child task message" }], + model: "gpt-3.5-turbo", + }); + return chatCompletion.choices[0].message.content; + } + + const result = await traceloop.withWorkflow( + { name: "test_child_propagation" }, + async () => { + // Set associations at the workflow level + traceloop.Associations.set([ + [traceloop.AssociationProperty.CONVERSATION_ID, "conv-propagate"], + [traceloop.AssociationProperty.USER_ID, "user-propagate"], + ]); + + // Call a child task + const taskResult = await subtask(); + + return taskResult; + }, ); - assert.strictEqual( - chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.customer_id` - ], - "customer-def", + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const workflowSpan = spans.find( + (span) => span.name === "test_child_propagation.workflow", ); + const taskSpan = spans.find((span) => span.name === "subtask.task"); + const chatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + "Child task message", + ); + + assert.ok(result); + assert.ok(workflowSpan); + assert.ok(taskSpan); + assert.ok(chatSpan); + + // All spans should have the associations (standard properties without prefix) assert.strictEqual( - chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id` - ], - "user-ghi", + workflowSpan.attributes["conversation_id"], + "conv-propagate", + ); + assert.strictEqual(workflowSpan.attributes["user_id"], "user-propagate"); + + assert.strictEqual(taskSpan.attributes["conversation_id"], "conv-propagate"); + assert.strictEqual(taskSpan.attributes["user_id"], "user-propagate"); + + assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-propagate"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-propagate"); + }); + + it("should merge associations from Associations.set() and withAssociationProperties()", async () => { + const result = await traceloop.withWorkflow( + { name: "test_merge_associations" }, + async () => { + // Set standard associations + traceloop.Associations.set([ + [traceloop.AssociationProperty.CONVERSATION_ID, "conv-merge"], + [traceloop.AssociationProperty.USER_ID, "user-merge"], + ]); + + // Add custom properties via withAssociationProperties + return await traceloop.withAssociationProperties( + { custom_field: "custom-value" }, + async () => { + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Test merge" }], + model: "gpt-3.5-turbo", + }); + return chatCompletion.choices[0].message.content; + }, + ); + }, ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const chatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + "Test merge", + ); + + assert.ok(result); + assert.ok(chatSpan); + + // Standard properties should be without prefix + assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-merge"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-merge"); + + // Custom property should have prefix assert.strictEqual( chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.custom_field` ], - "session-jkl", + "custom-value", ); }); }); From 4bf88082892f5f251292aa2094f6eaff59e96825 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:33:51 +0200 Subject: [PATCH 18/24] add tools --- .../src/sample_chatbot_interactive.ts | 104 ++++++++++++++++-- 1 file changed, 97 insertions(+), 7 deletions(-) diff --git a/packages/sample-app/src/sample_chatbot_interactive.ts b/packages/sample-app/src/sample_chatbot_interactive.ts index e932580b..b037baa2 100644 --- a/packages/sample-app/src/sample_chatbot_interactive.ts +++ b/packages/sample-app/src/sample_chatbot_interactive.ts @@ -1,7 +1,8 @@ import * as traceloop from "@traceloop/node-server-sdk"; import { openai } from "@ai-sdk/openai"; -import { streamText, CoreMessage } from "ai"; +import { streamText, CoreMessage, tool } from "ai"; import * as readline from "readline"; +import { z } from "zod"; import "dotenv/config"; @@ -25,6 +26,8 @@ const colors = { class InteractiveChatbot { private conversationHistory: CoreMessage[] = []; private rl: readline.Interface; + private conversationId: string; + private userId: string; constructor() { this.rl = readline.createInterface({ @@ -32,6 +35,9 @@ class InteractiveChatbot { output: process.stdout, prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`, }); + // Generate unique IDs for this session + this.conversationId = `conv-${Date.now()}`; + this.userId = `user-${Math.random().toString(36).substring(7)}`; } @traceloop.task({ name: "summarize_interaction" }) @@ -72,6 +78,12 @@ class InteractiveChatbot { @traceloop.workflow({ name: "chat_interaction" }) async processMessage(userMessage: string): Promise { + // Set associations for tracing + traceloop.Associations.set([ + [traceloop.AssociationProperty.CONVERSATION_ID, this.conversationId], + [traceloop.AssociationProperty.USER_ID, this.userId], + ]); + // Add user message to history this.conversationHistory.push({ role: "user", @@ -87,10 +99,85 @@ class InteractiveChatbot { { role: "system", content: - "You are a helpful AI assistant. Provide clear, concise, and friendly responses.", + "You are a helpful AI assistant with access to tools. Use the available tools when appropriate to provide accurate information. Provide clear, concise, and friendly responses.", }, ...this.conversationHistory, ], + tools: { + calculator: tool({ + description: + "Perform mathematical calculations. Supports basic arithmetic operations.", + parameters: z.object({ + expression: z + .string() + .describe("The mathematical expression to evaluate (e.g., '2 + 2' or '10 * 5')"), + }), + execute: async ({ expression }) => { + try { + // Simple safe eval for basic math (only allow numbers and operators) + const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, ""); + const result = eval(sanitized); + console.log( + `\n${colors.yellow}🔧 Calculator: ${expression} = ${result}${colors.reset}`, + ); + return { result, expression }; + } catch (error) { + return { error: "Invalid mathematical expression" }; + } + }, + }), + getCurrentWeather: tool({ + description: + "Get the current weather for a location. Use this when users ask about weather conditions.", + parameters: z.object({ + location: z.string().describe("The city and country, e.g., 'London, UK'"), + }), + execute: async ({ location }) => { + console.log( + `\n${colors.yellow}🔧 Weather: Checking weather for ${location}${colors.reset}`, + ); + // Simulated weather data + const weatherConditions = ["sunny", "cloudy", "rainy", "partly cloudy"]; + const condition = + weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; + const temperature = Math.floor(Math.random() * 30) + 10; // 10-40°C + return { + location, + temperature: `${temperature}°C`, + condition, + humidity: `${Math.floor(Math.random() * 40) + 40}%`, + }; + }, + }), + getTime: tool({ + description: + "Get the current date and time. Use this when users ask about the current time or date.", + parameters: z.object({ + timezone: z + .string() + .optional() + .describe("Optional timezone (e.g., 'America/New_York')"), + }), + execute: async ({ timezone }) => { + const now = new Date(); + const options: Intl.DateTimeFormatOptions = { + timeZone: timezone, + dateStyle: "full", + timeStyle: "long", + }; + const formatted = now.toLocaleString("en-US", options); + console.log( + `\n${colors.yellow}🔧 Time: ${formatted}${colors.reset}`, + ); + return { + datetime: formatted, + timestamp: now.toISOString(), + timezone: timezone || "local", + }; + }, + }), + }, + maxSteps: 5, experimental_telemetry: { isEnabled: true }, }); @@ -102,11 +189,14 @@ class InteractiveChatbot { console.log("\n"); - // Add assistant response to history - this.conversationHistory.push({ - role: "assistant", - content: fullResponse, - }); + // Wait for the full response to complete to get all messages including tool calls + const finalResult = await result.response; + + // Add all response messages (including tool calls and results) to history + // This ensures the conversation history includes the complete interaction + for (const message of finalResult.messages) { + this.conversationHistory.push(message); + } // Generate summary for this interaction await this.generateSummary(userMessage, fullResponse); From a5f3100f690ae74cf8feae7806921e2a033e69a3 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:25:35 +0200 Subject: [PATCH 19/24] pretty --- .../sample-app/src/sample_associations.ts | 8 +++-- .../src/sample_chatbot_interactive.ts | 19 +++++++--- .../src/lib/tracing/associations.ts | 8 +++-- .../traceloop-sdk/test/associations.test.ts | 35 ++++++++++--------- pnpm-lock.yaml | 10 ++---- 5 files changed, 47 insertions(+), 33 deletions(-) diff --git a/packages/sample-app/src/sample_associations.ts b/packages/sample-app/src/sample_associations.ts index 7f38e714..0779b331 100644 --- a/packages/sample-app/src/sample_associations.ts +++ b/packages/sample-app/src/sample_associations.ts @@ -112,7 +112,9 @@ async function customerServiceDemo() { }); console.log("Customer 1 (cust-001):"); - console.log(`Response: ${customer1Response.choices[0].message.content}\n`); + console.log( + `Response: ${customer1Response.choices[0].message.content}\n`, + ); // Customer 2 - Update associations for new customer traceloop.Associations.set([ @@ -131,7 +133,9 @@ async function customerServiceDemo() { }); console.log("Customer 2 (cust-002):"); - console.log(`Response: ${customer2Response.choices[0].message.content}\n`); + console.log( + `Response: ${customer2Response.choices[0].message.content}\n`, + ); }, ); } diff --git a/packages/sample-app/src/sample_chatbot_interactive.ts b/packages/sample-app/src/sample_chatbot_interactive.ts index b037baa2..ccd86e7f 100644 --- a/packages/sample-app/src/sample_chatbot_interactive.ts +++ b/packages/sample-app/src/sample_chatbot_interactive.ts @@ -110,7 +110,9 @@ class InteractiveChatbot { parameters: z.object({ expression: z .string() - .describe("The mathematical expression to evaluate (e.g., '2 + 2' or '10 * 5')"), + .describe( + "The mathematical expression to evaluate (e.g., '2 + 2' or '10 * 5')", + ), }), execute: async ({ expression }) => { try { @@ -130,16 +132,25 @@ class InteractiveChatbot { description: "Get the current weather for a location. Use this when users ask about weather conditions.", parameters: z.object({ - location: z.string().describe("The city and country, e.g., 'London, UK'"), + location: z + .string() + .describe("The city and country, e.g., 'London, UK'"), }), execute: async ({ location }) => { console.log( `\n${colors.yellow}🔧 Weather: Checking weather for ${location}${colors.reset}`, ); // Simulated weather data - const weatherConditions = ["sunny", "cloudy", "rainy", "partly cloudy"]; + const weatherConditions = [ + "sunny", + "cloudy", + "rainy", + "partly cloudy", + ]; const condition = - weatherConditions[Math.floor(Math.random() * weatherConditions.length)]; + weatherConditions[ + Math.floor(Math.random() * weatherConditions.length) + ]; const temperature = Math.floor(Math.random() * 30) + 10; // 10-40°C return { location, diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts index ea0f41a8..422e9a9e 100644 --- a/packages/traceloop-sdk/src/lib/tracing/associations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -17,7 +17,7 @@ export enum AssociationProperty { * Use this to check if a property should be set directly or with the TRACELOOP_ASSOCIATION_PROPERTIES prefix. */ export const STANDARD_ASSOCIATION_PROPERTIES = new Set( - Object.values(AssociationProperty) + Object.values(AssociationProperty), ); /** @@ -48,7 +48,9 @@ export class Associations { // Get current associations from context or create empty object const existingAssociations = otelContext .active() - .getValue(ASSOCATION_PROPERTIES_KEY) as Record | undefined; + .getValue(ASSOCATION_PROPERTIES_KEY) as + | Record + | undefined; const currentAssociations: Record = existingAssociations ? { ...existingAssociations } : {}; @@ -65,7 +67,7 @@ export class Associations { // Set the new context as active using the context manager // This is the equivalent of Python's attach(set_value(...)) - const contextManager = (otelContext as any)['_getContextManager'](); + const contextManager = (otelContext as any)["_getContextManager"](); if ( contextManager && contextManager instanceof AsyncLocalStorageContextManager diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts index 07315bfd..4959b953 100644 --- a/packages/traceloop-sdk/test/associations.test.ts +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -111,10 +111,7 @@ describe("Test Associations API", () => { assert.ok(chatSpan); // Check that the association is set on both workflow and LLM spans - assert.strictEqual( - workflowSpan.attributes["conversation_id"], - "conv-123", - ); + assert.strictEqual(workflowSpan.attributes["conversation_id"], "conv-123"); assert.strictEqual( chatSpan.attributes[ `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.conversation_id` @@ -276,15 +273,6 @@ describe("Test Associations API", () => { }); it("should propagate associations to all child spans", async () => { - @traceloop.task({ name: "subtask" }) - async function subtask() { - const chatCompletion = await openai.chat.completions.create({ - messages: [{ role: "user", content: "Child task message" }], - model: "gpt-3.5-turbo", - }); - return chatCompletion.choices[0].message.content; - } - const result = await traceloop.withWorkflow( { name: "test_child_propagation" }, async () => { @@ -295,7 +283,16 @@ describe("Test Associations API", () => { ]); // Call a child task - const taskResult = await subtask(); + const taskResult = await traceloop.withTask( + { name: "subtask" }, + async () => { + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Child task message" }], + model: "gpt-3.5-turbo", + }); + return chatCompletion.choices[0].message.content; + }, + ); return taskResult; }, @@ -327,10 +324,16 @@ describe("Test Associations API", () => { ); assert.strictEqual(workflowSpan.attributes["user_id"], "user-propagate"); - assert.strictEqual(taskSpan.attributes["conversation_id"], "conv-propagate"); + assert.strictEqual( + taskSpan.attributes["conversation_id"], + "conv-propagate", + ); assert.strictEqual(taskSpan.attributes["user_id"], "user-propagate"); - assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-propagate"); + assert.strictEqual( + chatSpan.attributes["conversation_id"], + "conv-propagate", + ); assert.strictEqual(chatSpan.attributes["user_id"], "user-propagate"); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97dfea22..a4e41dbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3441,10 +3441,6 @@ packages: resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} - engines: {node: '>=14'} - '@phenomnomnominal/tsquery@5.0.1': resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==} peerDependencies: @@ -11622,8 +11618,6 @@ snapshots: '@opentelemetry/semantic-conventions@1.38.0': {} - '@opentelemetry/semantic-conventions@1.38.0': {} - '@phenomnomnominal/tsquery@5.0.1(typescript@5.9.3)': dependencies: esquery: 1.6.0 @@ -14384,7 +14378,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.3 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.13.2(debug@4.4.3)) + retry-axios: 2.6.0(axios@1.13.2) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -16326,7 +16320,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.13.2(debug@4.4.3)): + retry-axios@2.6.0(axios@1.13.2): dependencies: axios: 1.13.2(debug@4.4.3) From 5056da6b4084f679801d78bd121b893705651346 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:18:34 +0200 Subject: [PATCH 20/24] fix test --- .../recording.har | 490 ++++++++++++++++++ .../recording.har | 252 +++++++++ .../recording.har | 252 +++++++++ .../recording.har | 252 +++++++++ .../recording.har | 252 +++++++++ .../recording.har | 252 +++++++++ .../src/lib/tracing/span-processor.ts | 15 +- .../traceloop-sdk/test/associations.test.ts | 52 +- 8 files changed, 1775 insertions(+), 42 deletions(-) create mode 100644 packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har create mode 100644 packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har create mode 100644 packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har create mode 100644 packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har create mode 100644 packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har create mode 100644 packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har new file mode 100644 index 00000000..1f1160a3 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har @@ -0,0 +1,490 @@ +{ + "log": { + "_recordingName": "Test Associations API/should allow updating associations mid-workflow", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "b8201bbe07e5b4424a2de4e8d0641034", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 118, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "118" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"First message\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 567, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 567, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLBThsxEL3vV0x9TlDSFEhzQYgLSAiq9tBDhVZee3bj4vVY9jhthPLvlb2QXQqVetnDvHlv33uepwpAGC02INRWsuq9nV/5td9h1zf33+6/f212XxZ3a9fd+svUXiUxywxqfqLiF9aJot5bZENugFVAyZhVl+dnZ6vFp/PVsgA9abSZ1nmer05O55xCQ/PF8uPpM3NLRmEUG/hRAQA8lW/26DT+FhtYzF4mPcYoOxSb4xKACGTzRMgYTWTpWMxGUJFjdMX2NVpLH+CafoGSDm5gIMCeEjBpub+YEgO2Kcps3CVrJ4B0jljm4MXywzNyOJq01PlATfyLKlrjTNzWAWUklw1FJi8KeqgAHkoZ6VU+4QP1nmumRyy/+zyoibH9txgTSzuOl+vZO1q1RpbGxkmVQkm1RT0yx95l0oYmQDVJ/NbLe9pDauO6/5EfAaXQM+raB9RGvc47rgXMp/mvtWPDxbCIGHZGYc0GQ34Fja1MdjgaEfeRsa9b4zoMPphyOfkVq0P1BwAA//8DAN8UoaI4AwAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:12.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "drQBKJgNGAaQD9fx_Sh4yvCm1TL4Lnf7ea0tIbVnM.s-1766304732-1.0.1.1-t_a9BGMEbSboYtJot14QiUPavmOUtifHD6kZbgfGrP38t.kKEjvoSmX57xhbmfbFwS6tmmio91N.sScmmVaHaFoQB4aNu7FelgZnz1I8qJQ" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "VDjvnYY9_JyEWLpcxFbtOpeLdOgST3AsJ0UZj0xEc0w-1766304732304-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:12 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "683" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "702" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_70b4720225c4462ab219ae7a16e4d6e6" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=drQBKJgNGAaQD9fx_Sh4yvCm1TL4Lnf7ea0tIbVnM.s-1766304732-1.0.1.1-t_a9BGMEbSboYtJot14QiUPavmOUtifHD6kZbgfGrP38t.kKEjvoSmX57xhbmfbFwS6tmmio91N.sScmmVaHaFoQB4aNu7FelgZnz1I8qJQ; path=/; expires=Sun, 21-Dec-25 08:42:12 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=VDjvnYY9_JyEWLpcxFbtOpeLdOgST3AsJ0UZj0xEc0w-1766304732304-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9bb78af3120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:11.301Z", + "time": 873, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 873 + } + }, + { + "_id": "e3af0e6e5b79fe802c15b8d10a0a6ce7", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 119, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "119" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Second message\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 575, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 575, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLBbtswDL37KzidkyJplnbNZRhWoN2wWy8DhsJQJNrWJouKRK8zivz7IDmJnbUFdtGBj49675HPBYAwWmxAqEayar2df/Yf/NO3uNt9f+hvq4fVum+2X+8arpqw+yRmiUHbn6j4yLpQ1HqLbMgNsAooGdPU5fXV1Wrx/np1mYGWNNpEqz3PVxfrOXdhS/PF8nJ9YDZkFEaxgR8FAMBzfpNGp/GP2MBidqy0GKOsUWxOTQAikE0VIWM0kaVjMRtBRY7RZdn3aC2BrKVx7+CenkBJB19goEFPHTBp2X+c0gNWXZRJvuusnQDSOWKZ7Gfhjwdkf5JqqfaBtvEfqqiMM7EpA8pILsmKTF5kdF8APOZIujOXwgdqPZdMvzB/dzNME+MORmx5SEswsbST+pF0NqzUyNLYOElUKKka1CNzjF922tAEKCaWX4p5bfZg27j6f8aPgFLoGXXpA2qjzg2PbQHThb7Vdoo4CxYRw2+jsGSDIa1BYyU7O9yOiH1kbMvKuBqDDyYfUFpjsS/+AgAA//8DAPJCx24/AwAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:12.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "l85y3z7MNW9PxBfw4Kwk0qYEEb_Vqmnb10Z8Cd9A98g-1766304732-1.0.1.1-tlMQjujSxy08v8lBYRobWkK9NJtQVHKJ87a.vRTlbi4dFP4O9QDzCW.EsnH03L_pgIpA8Q0YhxqkFZmHr4sYaQfu0_2XVgAOeu4P3YAZfIA" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "sSypzEkvJTwON3Coh94tDZkUPtzkoQiIELhKYCEg1lQ-1766304732796-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:12 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "287" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "308" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999994" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_80ce64d903724fd2802c8abc3924c0c9" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=l85y3z7MNW9PxBfw4Kwk0qYEEb_Vqmnb10Z8Cd9A98g-1766304732-1.0.1.1-tlMQjujSxy08v8lBYRobWkK9NJtQVHKJ87a.vRTlbi4dFP4O9QDzCW.EsnH03L_pgIpA8Q0YhxqkFZmHr4sYaQfu0_2XVgAOeu4P3YAZfIA; path=/; expires=Sun, 21-Dec-25 08:42:12 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=sSypzEkvJTwON3Coh94tDZkUPtzkoQiIELhKYCEg1lQ-1766304732796-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9c0fe2d3120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:12.179Z", + "time": 492, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 492 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har new file mode 100644 index 00000000..8354fa84 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should merge associations from Associations.set() and withAssociationProperties()", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "589055754a2a0ad0a24f03993fd04563", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 115, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "115" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Test merge\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 594, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 594, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFJNb9swDL37V3A6J0U+lqbNZRgyYOmpl2GXoTAUibHVyaIg0W29Iv99kJzE7tYBu+jAx/f0HsnXAkAYLTYgVC1ZNd5Ot/7G/5rfVJ25/cK43t4xqs9P949fO/n9m5gkBu0fUfGZdaWo8RbZkOthFVAyJtX5+vp6Ofu4Xq4y0JBGm2iV5+nyajXlNuxpOpsvVidmTUZhFBv4UQAAvOY3eXQaX8QGZpNzpcEYZYVic2kCEIFsqggZo4ksHYvJACpyjC7b3qG19AF29AxKOriDngAdtcCkZQfPhmvgGqHBUCEwRv40Fgt4aKNMYVxr7QiQzhHLNIwc4+GEHC/GLVU+0D7+QRUH40ysy4AykksmI5MXGT0WAA95QO2bzMIHajyXTD8xf3fbq4lhIwM2X55AJpZ2qC8Wk3fESo0sjY2j+QolVY16YA7LkK02NAKKUeS/zbyn3cc2rvof+QFQCj2jLn1AbdTbwENbwHSv/2q7jDgbFhHDk1FYssGQ1qDxIFvbX5KIXWRsyoNxFQYfTD6ntMbiWPwGAAD//w==\",\"AwD98dtiTQMAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:15.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "KJUjBzo7Dsrcvl_dw4Q7swXJF0np7yCFp1zjKkmrUhA-1766304735-1.0.1.1-8vnqK4zxhftvMjQA9a6mjZ.SHUUudovr5.oiwis5RWgiV2IsuOhnYmlHqnNURmePzT.NyyeKtAez7INaY9Kqq4W2Lll2qcTqg3BCLV0.5FM" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "_kEzHtvd4Vo2ruw2lqo_P_HFcY2i_.i7npO7g2S2rvU-1766304735907-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:15 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "719" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "738" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999995" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_428744323ddb46bda6f34756c1ed5767" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=KJUjBzo7Dsrcvl_dw4Q7swXJF0np7yCFp1zjKkmrUhA-1766304735-1.0.1.1-8vnqK4zxhftvMjQA9a6mjZ.SHUUudovr5.oiwis5RWgiV2IsuOhnYmlHqnNURmePzT.NyyeKtAez7INaY9Kqq4W2Lll2qcTqg3BCLV0.5FM; path=/; expires=Sun, 21-Dec-25 08:42:15 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=_kEzHtvd4Vo2ruw2lqo_P_HFcY2i_.i7npO7g2S2rvU-1766304735907-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9d1cd883120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:14.863Z", + "time": 915, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 915 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har new file mode 100644 index 00000000..200adf55 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should propagate associations to all child spans", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "63d8627010f393d41028514b6da29bb3", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 123, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "123" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Child task message\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 579, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 579, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLLbtswELzrK7Y824EV51H4EqBGkRRpv6AIBJpcWawpLsNd1TUC/3tB+SG5DyAXHnZ2ljOz+1YAKGfVApRptJg2+ukyfoy78nVTPn3lx9fPtX9+/GK/beP2k1k+q0lm0OoHGjmxrgy10aM4CgfYJNSCeWp5f3c3n93cz296oCWLPtPWUabzq9updGlF01l5fXtkNuQMslrA9wIA4K1/s8Zg8ZdawGxyqrTIrNeoFucmAJXI54rSzI5FB1GTATQUBEMv+wm9pw+w1AF21EH0qBmhQR+hRdg6aUAaxyCaNw/jGQnrjnX2EDrvR4AOgUTnDHr1L0dkf9braR0TrfgPqqpdcNxUCTVTyNpYKKoe3RcAL30u3YVVFRO1USqhDfbflcdY1LCJEVgeQSHRfqhfn+oX0yqLop3nUa7KaNOgHZjDEnRnHY2AYuT5bzH/mn3w7cL6PeMHwBiMgraKCa0zl4aHtoT5Tv/Xds64F6wY009nsBKHKe/BYq07f7ggxTsWbKvahTWmmFx/RnmPxb74DQAA//8DAMn4bMZFAwAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:14.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "tdhwk6A4993pgei3vC4TMyw..OHxv5LlEVoncpvJiCw-1766304734-1.0.1.1-0Q6dipDEB.6k3wF6ZVE_WXQv0eAuwI56rC.PaK5tNpkdzE0bHTLCkVv1ImN4WERZjB.bl3Zjw_cr1.WOloR6qy6moqPfO5Veo6Ws242Dluo" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "2hMhu8GIIzuEDZgdnwraFL7CHWtgrCBOTsg8QebSapE-1766304734979-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:14 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "314" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "340" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_847ecbd28c8d473986aef4100921c5a8" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=tdhwk6A4993pgei3vC4TMyw..OHxv5LlEVoncpvJiCw-1766304734-1.0.1.1-0Q6dipDEB.6k3wF6ZVE_WXQv0eAuwI56rC.PaK5tNpkdzE0bHTLCkVv1ImN4WERZjB.bl3Zjw_cr1.WOloR6qy6moqPfO5Veo6Ws242Dluo; path=/; expires=Sun, 21-Dec-25 08:42:14 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=2hMhu8GIIzuEDZgdnwraFL7CHWtgrCBOTsg8QebSapE-1766304734979-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9ce7ae73120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:14.334Z", + "time": 521, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 521 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har new file mode 100644 index 00000000..5c8a6293 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should set multiple associations on spans", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "aad89c290c4c49a352550af1f3ee34d3", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 119, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "119" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Tell me a fact\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 686, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 686, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFNNb9swDL3nVxA6J0HTLO2W2zBgxdDLMHQDhrUwZJm2ucqiINLpmiL/fbCSxck+gF184ON7Jt+jXiYAhiqzBuNaq66LfvYuvo7959s73HS3z+/d18vtzZeSPm4/3DRRzHRgcPkdnf5izR130aMShz3sElrFQXVxfXW1vHh1vbzIQMcV+oHWRJ0t56uZ9qnk2cXicnVgtkwOxazh2wQA4CV/hxlDhT/MGrJOrnQoYhs062MTgEnsh4qxIiRqg5rpCDoOiiGPfdciROt6qEnaKXCtGCBhjSlhBcpgBbRFuDel9R5cr4rp3hzaSeAx8FOAmhOQCkhrUwRF1BZsqKC1JSlwDSUphQa4rrOcoig5jzJgnfUI8kRdh0mAQu5429ktB/hEG0zz0+ET1r3YwbzQe38C2BBY7WB+tu3hgOyORnluYuJSfqOamgJJWyS0wmEwRZSjyehuAvCQA+nPPDYxcRe1UH7E/LvFYi9nxhMYweWbA6is1o/11SHEc7WiQrXk5SRQ46xrsRqZY/q2r4hPgMnJzn8O8zft/d4Umv+RHwHnMCpWRUxYkTtfeGxLODyQf7UdPc4DG8G0IYeFEqYhhwpr2/v96Rp5FsWuqCk0mGKifL9DjpPd5CcAAAD//w==\",\"AwDQ8+nOvgMAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:11.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "B1BMLT_q7zMT0idIrO2eGScOPXTM52s8Wf0fr4JaDuA-1766304731-1.0.1.1-MsQDbQVW2CgDOl9coYCKtKVztnCWqB7UHPtMVsFrms4gffsMMmdZn.3Ep4a9tboSJgizgb7DUYCezESnxFoag5_1M4Vzyxhiz0tmRIieMJc" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "SzvXQOANAG_KoWCMpsxs27nZ0V8VshPzKTnjF_MSYCU-1766304731424-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:11 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "770" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "788" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_f613030df3ae44389bebfcf6fe36da91" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=B1BMLT_q7zMT0idIrO2eGScOPXTM52s8Wf0fr4JaDuA-1766304731-1.0.1.1-MsQDbQVW2CgDOl9coYCKtKVztnCWqB7UHPtMVsFrms4gffsMMmdZn.3Ep4a9tboSJgizgb7DUYCezESnxFoag5_1M4Vzyxhiz0tmRIieMJc; path=/; expires=Sun, 21-Dec-25 08:42:11 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=SzvXQOANAG_KoWCMpsxs27nZ0V8VshPzKTnjF_MSYCU-1766304731424-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9b55aad3120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:10.318Z", + "time": 977, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 977 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har new file mode 100644 index 00000000..53b24cb5 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should set single association on spans", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "fd40a5569f36530000ae54cd2be33f61", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 119, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "119" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Tell me a joke\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 610, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 610, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLBbhMxEL3vVww+J1XS0LTKBQlu0AsHBAhVq4k92R3qtS173LSq8u/ITshuoUhcfJg37/m9mXluABQbtQGlexQ9BDv/EG5C5s9xteBP+cvtevv4XS++PWTd3X+8VbPC8NufpOU360L7IVgS9u4I60goVFSX1+v1avH2erWowOAN2ULrgsxXF1dzyXHr54vl5dWJ2XvWlNQGfjQAAM/1LR6doUe1gapTKwOlhB2pzbkJQEVvS0VhSpwEnajZCGrvhFy1/bV/AsMGpCdIGiPp6PewZwfoAPcYzTt4TxpzIugJ9pjAZymKhl0H7KDnBDsma95Mv4i0ywlLRJetnQDonBcsI6rh7k7I4RzH+i5Ev01/UNWOHae+jYTJu2I9iQ+qoocG4K6OLb+YhArRD0Fa8fdUv1suj3JqXNQEvD6B4gXtWL+8mb2i1hoSZJsmY1cadU9mZI47wmzYT4BmkvlvM69pH3Oz6/5HfgS0piBk2hDJsH4ZeGyLVM74X23nGVfDKlF8YE2tMMWyB0M7zPZ4YCo9JaGh3bHrKIbI9crKHptD8wsAAP//\",\"AwBD1GbaZAMAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:10.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "ZzRza1aEuHO9cVzRkHd6mpdGea7.3G8.d4IH2o6KZrs-1766304730-1.0.1.1-DbIp.Oy_SLvokBhLUif.WRPuTZYxCu2V_jJDbJeNVFWb9FfS9j.ljPMdHQBdHkrX6TkweOLdUIwiLRrtJkHioP7VIRRBIHkWQjaDEz.HYug" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "XPfe5DDAEfiuv6VfH4DKHkroWn1jOONENTK.UTDSWoY-1766304730436-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:10 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "248" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "268" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_d335f511512c4eeb96259b5aa46677df" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=ZzRza1aEuHO9cVzRkHd6mpdGea7.3G8.d4IH2o6KZrs-1766304730-1.0.1.1-DbIp.Oy_SLvokBhLUif.WRPuTZYxCu2V_jJDbJeNVFWb9FfS9j.ljPMdHQBdHkrX6TkweOLdUIwiLRrtJkHioP7VIRRBIHkWQjaDEz.HYug; path=/; expires=Sun, 21-Dec-25 08:42:10 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=XPfe5DDAEfiuv6VfH4DKHkroWn1jOONENTK.UTDSWoY-1766304730436-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9b288503120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:09.832Z", + "time": 477, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 477 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har new file mode 100644 index 00000000..68bc023c --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should work with all AssociationProperty types", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "86b47881e1313cfde7039fb11eebc151", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 124, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "124" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Test all properties\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 1202, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 1202, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"nFVNj9w2DL3PryB89g72M2nmuihatGibAgF6aIIBLdNjtrKoiPTuToL974XkmbG36SLZXgZjPT3xURQfP68AKm6rDVSuR3ND9Ge38bt4n34mefP7w8P++ofbPwLvPv163fFPb11VZ4Y0f5GzI2vtZIiejCVMsEuERvnUi9evXl2dX7++uizAIC35TNtFO7ta35zZmBo5O7+4vDkwe2FHWm3gzxUAwOfymzWGlh6qDZzXx5WBVHFH1ea0CaBK4vNKhaqshsGqegadBKNQZHMA6wnuJfn2fXgf3vWUCDARNOw9S1CQDmKSSMmYFJaEGjC0wAaswEMUVW48gQkYqeV9A6D3a/hR7umOUg0qA4GTYZCwPNR6NHAYoKFCpRY4OD+2tMmiLtbwtt8rO/QL1gbe9aR03AlolrgZjRR0dD2gghMvOSp/ohq0x0g1GD3YmGjSfk+86219OGmh6CCGUNnvj5qaPUijlO4wlxgkwUCoY6KBgq2z0ss13PY0vFxpInTGd2z7GjqPw4AN+/KFjtvpT2hBxY8T8Lzmg1jrk4y7HtxRzxSiVDQBBvR7ZS2qr9bwC7kew8t1qyUKO+tr6DG1gVRrII9q7E6iLevI0DdrHmY1GeGwg4Gsl3YObBSUPZ1wSScFx7WS23UJmoaXP53QjqeS0EPEoCzhUIZIjjt20BPaV7Nq9qBjMYosNHfPgEaJc3YCLXcdJQoGRkOkhPl1lvA8VSsHnN5ZprPlx6JRglJJ8GYN33tylv5PeyxzTJSt4vCRgzqM6NgwOPpqjqNmcTQLOdaFPo4cT/3x2x0l9L6G+z4Xb7KOIAZfmAd6/7ztzJaTsrctHQcSebrLiwu2dIDzpUvK9dCSWKZSyD1cbvbjiLm5piKPbIcuhE5SwTkbZ1vypfXSUhN1o2K29DB6vwAwBLHiF8XMPxyQx5N9e9nFJI3+i1p1HFj7bSJUCdmq1SRWBX1cAXwoY2J84vxVTDJE25r8TSXcxWFKVPNgmsHL68sDamLoF8DNEXhy3rYlQ/a6GDSVQ9dTO1PnqYRjy7IAVousv5TzX2dPmXPYfcvxM+AcRaN2GxO17J6mPG9LlPvxuW2nWy6Cq2z57GhrTClXoqUORz+N1Er3ajRsOw47SjFxmau5kqvH1T8AAAD//w==\",\"AwD3pwbhVggAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:14.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "LXl7YIkHSftn90NVEMYr9u6WFrNiB2unH2IfC0lAnuU-1766304734-1.0.1.1-L8ZO_bqTrLcFXCTsY2cEM2QUT_ibEzq3v1WOwJqF8SExpgHfC9f87tgPHFF2sZ5GFqwPeFIqZoLAR8zx_rsl1pEosKCQBP4q7leynQ5AuOE" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "rteD6Fb73g85ujX.BasqlUzcnlFu6Wcm76q2AAEuPd4-1766304734441-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:14 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "1434" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "1453" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999992" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_9c41fddbe63f4a14a3729d050b59ef8c" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=LXl7YIkHSftn90NVEMYr9u6WFrNiB2unH2IfC0lAnuU-1766304734-1.0.1.1-L8ZO_bqTrLcFXCTsY2cEM2QUT_ibEzq3v1WOwJqF8SExpgHfC9f87tgPHFF2sZ5GFqwPeFIqZoLAR8zx_rsl1pEosKCQBP4q7leynQ5AuOE; path=/; expires=Sun, 21-Dec-25 08:42:14 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=rteD6Fb73g85ujX.BasqlUzcnlFu6Wcm76q2AAEuPd4-1766304734441-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9c419a23120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1323, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:12.680Z", + "time": 1636, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 1636 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts index 252b86ff..d05f0b1c 100644 --- a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts +++ b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts @@ -15,6 +15,7 @@ import { AGENT_NAME_KEY, } from "./tracing"; import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; +import { STANDARD_ASSOCIATION_PROPERTIES } from "./associations"; import { ATTR_GEN_AI_AGENT_NAME } from "@opentelemetry/semantic-conventions/incubating"; import { transformAiSdkSpanAttributes, @@ -200,10 +201,16 @@ const onSpanStart = (span: Span): void => { .getValue(ASSOCATION_PROPERTIES_KEY); if (associationProperties) { for (const [key, value] of Object.entries(associationProperties)) { - span.setAttribute( - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`, - value, - ); + // Standard association properties are set without prefix on all spans + if (STANDARD_ASSOCIATION_PROPERTIES.has(key)) { + span.setAttribute(key, value); + } else { + // Custom properties use the TRACELOOP_ASSOCIATION_PROPERTIES prefix + span.setAttribute( + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`, + value, + ); + } } } diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts index 4959b953..8749f06e 100644 --- a/packages/traceloop-sdk/test/associations.test.ts +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -25,6 +25,7 @@ import NodeHttpAdapter from "@pollyjs/adapter-node-http"; import FetchAdapter from "@pollyjs/adapter-fetch"; import FSPersister from "@pollyjs/persister-fs"; import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; +import { ATTR_GEN_AI_PROMPT } from "@opentelemetry/semantic-conventions/incubating"; import { initializeSharedTraceloop, getSharedExporter } from "./test-setup"; const memoryExporter = getSharedExporter(); @@ -110,14 +111,9 @@ describe("Test Associations API", () => { assert.ok(workflowSpan); assert.ok(chatSpan); - // Check that the association is set on both workflow and LLM spans + // Check that the association is set on both workflow and LLM spans (standard properties without prefix) assert.strictEqual(workflowSpan.attributes["conversation_id"], "conv-123"); - assert.strictEqual( - chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.conversation_id` - ], - "conv-123", - ); + assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-123"); }); it("should set multiple associations on spans", async () => { @@ -151,21 +147,11 @@ describe("Test Associations API", () => { assert.ok(workflowSpan); assert.ok(chatSpan); - // Check that both associations are set + // Check that both associations are set (standard properties without prefix) assert.strictEqual(workflowSpan.attributes["user_id"], "user-456"); assert.strictEqual(workflowSpan.attributes["session_id"], "session-789"); - assert.strictEqual( - chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.user_id` - ], - "user-456", - ); - assert.strictEqual( - chatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` - ], - "session-789", - ); + assert.strictEqual(chatSpan.attributes["user_id"], "user-456"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-789"); }); it("should allow updating associations mid-workflow", async () => { @@ -205,13 +191,13 @@ describe("Test Associations API", () => { const firstChatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "First message", ); const secondChatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Second message", ); @@ -219,21 +205,11 @@ describe("Test Associations API", () => { assert.ok(firstChatSpan); assert.ok(secondChatSpan); - // First span should have initial value - assert.strictEqual( - firstChatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` - ], - "session-initial", - ); + // First span should have initial value (standard properties without prefix) + assert.strictEqual(firstChatSpan.attributes["session_id"], "session-initial"); - // Second span should have updated value - assert.strictEqual( - secondChatSpan.attributes[ - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.session_id` - ], - "session-updated", - ); + // Second span should have updated value (standard properties without prefix) + assert.strictEqual(secondChatSpan.attributes["session_id"], "session-updated"); }); it("should work with all AssociationProperty types", async () => { @@ -308,7 +284,7 @@ describe("Test Associations API", () => { const chatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Child task message", ); @@ -367,7 +343,7 @@ describe("Test Associations API", () => { const chatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${SpanAttributes.ATTR_GEN_AI_PROMPT}.0.content`] === + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Test merge", ); From 0bd19e0588e01275559552f73cea35045d3f47bb Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:24:52 +0200 Subject: [PATCH 21/24] pretty --- .../traceloop-sdk/test/associations.test.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts index 8749f06e..c66f8a7e 100644 --- a/packages/traceloop-sdk/test/associations.test.ts +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -191,14 +191,12 @@ describe("Test Associations API", () => { const firstChatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === - "First message", + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "First message", ); const secondChatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === - "Second message", + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Second message", ); assert.ok(result); @@ -206,10 +204,16 @@ describe("Test Associations API", () => { assert.ok(secondChatSpan); // First span should have initial value (standard properties without prefix) - assert.strictEqual(firstChatSpan.attributes["session_id"], "session-initial"); + assert.strictEqual( + firstChatSpan.attributes["session_id"], + "session-initial", + ); // Second span should have updated value (standard properties without prefix) - assert.strictEqual(secondChatSpan.attributes["session_id"], "session-updated"); + assert.strictEqual( + secondChatSpan.attributes["session_id"], + "session-updated", + ); }); it("should work with all AssociationProperty types", async () => { @@ -343,8 +347,7 @@ describe("Test Associations API", () => { const chatSpan = spans.find( (span) => span.name === "openai.chat" && - span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === - "Test merge", + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Test merge", ); assert.ok(result); From 36c7cb22098a6934a5a8a722c465e284535aab05 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:47:08 +0200 Subject: [PATCH 22/24] spelling --- packages/traceloop-sdk/src/lib/tracing/association.ts | 6 +++--- packages/traceloop-sdk/src/lib/tracing/associations.ts | 6 +++--- packages/traceloop-sdk/src/lib/tracing/decorators.ts | 4 ++-- packages/traceloop-sdk/src/lib/tracing/span-processor.ts | 4 ++-- packages/traceloop-sdk/src/lib/tracing/tracing.ts | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/traceloop-sdk/src/lib/tracing/association.ts b/packages/traceloop-sdk/src/lib/tracing/association.ts index 44d71b5e..5cb5e803 100644 --- a/packages/traceloop-sdk/src/lib/tracing/association.ts +++ b/packages/traceloop-sdk/src/lib/tracing/association.ts @@ -1,5 +1,5 @@ import { context } from "@opentelemetry/api"; -import { ASSOCATION_PROPERTIES_KEY } from "./tracing"; +import { ASSOCIATION_PROPERTIES_KEY } from "./tracing"; export function withAssociationProperties< A extends unknown[], @@ -17,13 +17,13 @@ export function withAssociationProperties< // Get existing associations from context and merge with new properties const existingAssociations = context .active() - .getValue(ASSOCATION_PROPERTIES_KEY) as Record | undefined; + .getValue(ASSOCIATION_PROPERTIES_KEY) as Record | undefined; const mergedAssociations = existingAssociations ? { ...existingAssociations, ...properties } : properties; const newContext = context .active() - .setValue(ASSOCATION_PROPERTIES_KEY, mergedAssociations); + .setValue(ASSOCIATION_PROPERTIES_KEY, mergedAssociations); return context.with(newContext, fn, thisArg, ...args); } diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts index 422e9a9e..a5de222c 100644 --- a/packages/traceloop-sdk/src/lib/tracing/associations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -1,6 +1,6 @@ import { trace, context as otelContext } from "@opentelemetry/api"; import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; -import { ASSOCATION_PROPERTIES_KEY } from "./tracing"; +import { ASSOCIATION_PROPERTIES_KEY } from "./tracing"; /** * Standard association properties for tracing. @@ -48,7 +48,7 @@ export class Associations { // Get current associations from context or create empty object const existingAssociations = otelContext .active() - .getValue(ASSOCATION_PROPERTIES_KEY) as + .getValue(ASSOCIATION_PROPERTIES_KEY) as | Record | undefined; const currentAssociations: Record = existingAssociations @@ -63,7 +63,7 @@ export class Associations { // Store associations in context const newContext = otelContext .active() - .setValue(ASSOCATION_PROPERTIES_KEY, currentAssociations); + .setValue(ASSOCIATION_PROPERTIES_KEY, currentAssociations); // Set the new context as active using the context manager // This is the equivalent of Python's attach(set_value(...)) diff --git a/packages/traceloop-sdk/src/lib/tracing/decorators.ts b/packages/traceloop-sdk/src/lib/tracing/decorators.ts index aaf0f02b..3eeddcb5 100644 --- a/packages/traceloop-sdk/src/lib/tracing/decorators.ts +++ b/packages/traceloop-sdk/src/lib/tracing/decorators.ts @@ -2,7 +2,7 @@ import { Span, context } from "@opentelemetry/api"; import { suppressTracing } from "@opentelemetry/core"; import { AGENT_NAME_KEY, - ASSOCATION_PROPERTIES_KEY, + ASSOCIATION_PROPERTIES_KEY, ENTITY_NAME_KEY, getEntityPath, getTracer, @@ -71,7 +71,7 @@ function withEntity< } if (associationProperties) { entityContext = entityContext.setValue( - ASSOCATION_PROPERTIES_KEY, + ASSOCIATION_PROPERTIES_KEY, associationProperties, ); } diff --git a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts index d05f0b1c..ba48af76 100644 --- a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts +++ b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts @@ -9,7 +9,7 @@ import { context } from "@opentelemetry/api"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; import { SpanExporter } from "@opentelemetry/sdk-trace-base"; import { - ASSOCATION_PROPERTIES_KEY, + ASSOCIATION_PROPERTIES_KEY, ENTITY_NAME_KEY, WORKFLOW_NAME_KEY, AGENT_NAME_KEY, @@ -198,7 +198,7 @@ const onSpanStart = (span: Span): void => { const associationProperties = context .active() - .getValue(ASSOCATION_PROPERTIES_KEY); + .getValue(ASSOCIATION_PROPERTIES_KEY); if (associationProperties) { for (const [key, value] of Object.entries(associationProperties)) { // Standard association properties are set without prefix on all spans diff --git a/packages/traceloop-sdk/src/lib/tracing/tracing.ts b/packages/traceloop-sdk/src/lib/tracing/tracing.ts index 5bb79951..8bbcc61b 100644 --- a/packages/traceloop-sdk/src/lib/tracing/tracing.ts +++ b/packages/traceloop-sdk/src/lib/tracing/tracing.ts @@ -4,7 +4,7 @@ const TRACER_NAME = "@traceloop/node-server-sdk"; export const WORKFLOW_NAME_KEY = createContextKey("workflow_name"); export const ENTITY_NAME_KEY = createContextKey("entity_name"); export const AGENT_NAME_KEY = createContextKey("agent_name"); -export const ASSOCATION_PROPERTIES_KEY = createContextKey( +export const ASSOCIATION_PROPERTIES_KEY = createContextKey( "association_properties", ); From d79c21baa9703a3e00ca03a9ed63f46fb68a3539 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Sun, 21 Dec 2025 11:24:59 +0200 Subject: [PATCH 23/24] no conversation id --- .../sample-app/src/sample_associations.ts | 8 +-- .../src/sample_chatbot_interactive.ts | 6 +-- .../src/lib/tracing/ai-sdk-transformations.ts | 13 ----- .../src/lib/tracing/associations.ts | 3 +- .../test/ai-sdk-transformations.test.ts | 51 ------------------- .../traceloop-sdk/test/associations.test.ts | 26 +++++----- 6 files changed, 18 insertions(+), 89 deletions(-) diff --git a/packages/sample-app/src/sample_associations.ts b/packages/sample-app/src/sample_associations.ts index 0779b331..1e6b3cc9 100644 --- a/packages/sample-app/src/sample_associations.ts +++ b/packages/sample-app/src/sample_associations.ts @@ -12,12 +12,11 @@ const openai = new OpenAI(); /** * Sample chatbot that demonstrates the Associations API. - * This example shows how to track conversations, users, and sessions + * This example shows how to track users and sessions * across multiple LLM interactions. */ class ChatbotWithAssociations { constructor( - private conversationId: string, private userId: string, private sessionId: string, ) {} @@ -28,14 +27,12 @@ class ChatbotWithAssociations { @traceloop.workflow({ name: "chatbot_conversation" }) async handleConversation() { console.log("\n=== Starting Chatbot Conversation ==="); - console.log(`Conversation ID: ${this.conversationId}`); console.log(`User ID: ${this.userId}`); console.log(`Session ID: ${this.sessionId}\n`); // Set standard associations at the beginning of the conversation // These will be automatically attached to all spans within this context traceloop.Associations.set([ - [traceloop.AssociationProperty.CONVERSATION_ID, this.conversationId], [traceloop.AssociationProperty.USER_ID, this.userId], [traceloop.AssociationProperty.SESSION_ID, this.sessionId], ]); @@ -151,7 +148,6 @@ async function main() { try { // Example 1: Multi-turn chatbot conversation with custom properties const chatbot = new ChatbotWithAssociations( - "conv-abc-123", // conversation_id "user-alice-456", // user_id "session-xyz-789", // session_id ); @@ -166,7 +162,7 @@ async function main() { "Check your Traceloop dashboard to see the associations attached to traces!", ); console.log( - "You can filter and search by conversation_id, user_id, session_id, customer_id, or custom properties like chat_subject.", + "You can filter and search by user_id, session_id, customer_id, or custom properties like chat_subject.", ); } catch (error) { console.error("Error running demo:", error); diff --git a/packages/sample-app/src/sample_chatbot_interactive.ts b/packages/sample-app/src/sample_chatbot_interactive.ts index ccd86e7f..d5c8f30d 100644 --- a/packages/sample-app/src/sample_chatbot_interactive.ts +++ b/packages/sample-app/src/sample_chatbot_interactive.ts @@ -26,7 +26,7 @@ const colors = { class InteractiveChatbot { private conversationHistory: CoreMessage[] = []; private rl: readline.Interface; - private conversationId: string; + private sessionId: string; private userId: string; constructor() { @@ -36,7 +36,7 @@ class InteractiveChatbot { prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`, }); // Generate unique IDs for this session - this.conversationId = `conv-${Date.now()}`; + this.sessionId = `session-${Date.now()}`; this.userId = `user-${Math.random().toString(36).substring(7)}`; } @@ -80,7 +80,7 @@ class InteractiveChatbot { async processMessage(userMessage: string): Promise { // Set associations for tracing traceloop.Associations.set([ - [traceloop.AssociationProperty.CONVERSATION_ID, this.conversationId], + [traceloop.AssociationProperty.SESSION_ID, this.sessionId], [traceloop.AssociationProperty.USER_ID, this.userId], ]); 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 2dd604c5..66a73f42 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -6,7 +6,6 @@ import { import { ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_COMPLETION, - ATTR_GEN_AI_CONVERSATION_ID, ATTR_GEN_AI_INPUT_MESSAGES, ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_OUTPUT_MESSAGES, @@ -565,17 +564,6 @@ const transformToolCallAttributes = (attributes: Record): void => { } }; -const transformConversationId = (attributes: Record): void => { - const conversationId = attributes["ai.telemetry.metadata.conversationId"]; - const sessionId = attributes["ai.telemetry.metadata.sessionId"]; - - if (conversationId) { - attributes[ATTR_GEN_AI_CONVERSATION_ID] = conversationId; - } else if (sessionId) { - attributes[ATTR_GEN_AI_CONVERSATION_ID] = sessionId; - } -}; - const transformResponseMetadata = (attributes: Record): void => { const AI_RESPONSE_MODEL = "ai.response.model"; const AI_RESPONSE_ID = "ai.response.id"; @@ -686,7 +674,6 @@ export const transformLLMSpans = ( transformResponseMetadata(attributes); calculateTotalTokens(attributes); transformVendor(attributes); // Also sets GEN_AI_PROVIDER_NAME - transformConversationId(attributes); transformToolCallAttributes(attributes); transformTelemetryMetadata(attributes, spanName); }; diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts index a5de222c..ecae7c48 100644 --- a/packages/traceloop-sdk/src/lib/tracing/associations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -6,7 +6,6 @@ import { ASSOCIATION_PROPERTIES_KEY } from "./tracing"; * Standard association properties for tracing. */ export enum AssociationProperty { - CONVERSATION_ID = "conversation_id", CUSTOMER_ID = "customer_id", USER_ID = "user_id", SESSION_ID = "session_id", @@ -36,7 +35,7 @@ export class Associations { * * @example * // Single association - * Associations.set([[AssociationProperty.CONVERSATION_ID, "conv-123"]]); + * Associations.set([[AssociationProperty.SESSION_ID, "session-123"]]); * * // Multiple associations * Associations.set([ diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 178c0de4..d288859b 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -4,7 +4,6 @@ import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; import { ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_COMPLETION, - ATTR_GEN_AI_CONVERSATION_ID, ATTR_GEN_AI_INPUT_MESSAGES, ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_OUTPUT_MESSAGES, @@ -2185,52 +2184,6 @@ describe("AI SDK Transformations", () => { }); }); - describe("transformLLMSpans - conversation id", () => { - it("should transform conversationId from metadata", () => { - const attributes = { - "ai.telemetry.metadata.conversationId": "conv_123", - }; - - transformLLMSpans(attributes); - - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], "conv_123"); - }); - - it("should use sessionId as fallback for conversation id", () => { - const attributes = { - "ai.telemetry.metadata.sessionId": "session_456", - }; - - transformLLMSpans(attributes); - - assert.strictEqual( - attributes[ATTR_GEN_AI_CONVERSATION_ID], - "session_456", - ); - }); - - it("should prefer conversationId over sessionId", () => { - const attributes = { - "ai.telemetry.metadata.conversationId": "conv_123", - "ai.telemetry.metadata.sessionId": "session_456", - }; - - transformLLMSpans(attributes); - - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], "conv_123"); - }); - - it("should not set conversation id when neither is present", () => { - const attributes = { - "ai.telemetry.metadata.userId": "user_789", - }; - - transformLLMSpans(attributes); - - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], undefined); - }); - }); - describe("transformLLMSpans - response metadata", () => { it("should transform ai.response.model to gen_ai.response.model", () => { const attributes = { @@ -2303,7 +2256,6 @@ describe("AI SDK Transformations", () => { ]), "ai.usage.promptTokens": 10, "ai.usage.completionTokens": 15, - "ai.telemetry.metadata.conversationId": "conv_456", "ai.telemetry.metadata.userId": "user_789", }; @@ -2332,9 +2284,6 @@ describe("AI SDK Transformations", () => { "chatcmpl-abc123", ); - // Check conversation ID - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], "conv_456"); - // Check that original AI SDK attributes are removed assert.strictEqual(attributes["ai.model.id"], undefined); assert.strictEqual(attributes["ai.model.provider"], undefined); diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts index c66f8a7e..4c2c971b 100644 --- a/packages/traceloop-sdk/test/associations.test.ts +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -87,7 +87,7 @@ describe("Test Associations API", () => { async () => { // Set a single association traceloop.Associations.set([ - [traceloop.AssociationProperty.CONVERSATION_ID, "conv-123"], + [traceloop.AssociationProperty.SESSION_ID, "session-123"], ]); const chatCompletion = await openai.chat.completions.create({ @@ -112,8 +112,8 @@ describe("Test Associations API", () => { assert.ok(chatSpan); // Check that the association is set on both workflow and LLM spans (standard properties without prefix) - assert.strictEqual(workflowSpan.attributes["conversation_id"], "conv-123"); - assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-123"); + assert.strictEqual(workflowSpan.attributes["session_id"], "session-123"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-123"); }); it("should set multiple associations on spans", async () => { @@ -222,7 +222,6 @@ describe("Test Associations API", () => { async () => { // Set all association property types traceloop.Associations.set([ - [traceloop.AssociationProperty.CONVERSATION_ID, "conv-abc"], [traceloop.AssociationProperty.CUSTOMER_ID, "customer-def"], [traceloop.AssociationProperty.USER_ID, "user-ghi"], [traceloop.AssociationProperty.SESSION_ID, "session-jkl"], @@ -246,7 +245,6 @@ describe("Test Associations API", () => { assert.ok(chatSpan); // Check all property types are set (standard properties without prefix) - assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-abc"); assert.strictEqual(chatSpan.attributes["customer_id"], "customer-def"); assert.strictEqual(chatSpan.attributes["user_id"], "user-ghi"); assert.strictEqual(chatSpan.attributes["session_id"], "session-jkl"); @@ -258,7 +256,7 @@ describe("Test Associations API", () => { async () => { // Set associations at the workflow level traceloop.Associations.set([ - [traceloop.AssociationProperty.CONVERSATION_ID, "conv-propagate"], + [traceloop.AssociationProperty.SESSION_ID, "session-propagate"], [traceloop.AssociationProperty.USER_ID, "user-propagate"], ]); @@ -299,20 +297,20 @@ describe("Test Associations API", () => { // All spans should have the associations (standard properties without prefix) assert.strictEqual( - workflowSpan.attributes["conversation_id"], - "conv-propagate", + workflowSpan.attributes["session_id"], + "session-propagate", ); assert.strictEqual(workflowSpan.attributes["user_id"], "user-propagate"); assert.strictEqual( - taskSpan.attributes["conversation_id"], - "conv-propagate", + taskSpan.attributes["session_id"], + "session-propagate", ); assert.strictEqual(taskSpan.attributes["user_id"], "user-propagate"); assert.strictEqual( - chatSpan.attributes["conversation_id"], - "conv-propagate", + chatSpan.attributes["session_id"], + "session-propagate", ); assert.strictEqual(chatSpan.attributes["user_id"], "user-propagate"); }); @@ -323,7 +321,7 @@ describe("Test Associations API", () => { async () => { // Set standard associations traceloop.Associations.set([ - [traceloop.AssociationProperty.CONVERSATION_ID, "conv-merge"], + [traceloop.AssociationProperty.SESSION_ID, "session-merge"], [traceloop.AssociationProperty.USER_ID, "user-merge"], ]); @@ -354,7 +352,7 @@ describe("Test Associations API", () => { assert.ok(chatSpan); // Standard properties should be without prefix - assert.strictEqual(chatSpan.attributes["conversation_id"], "conv-merge"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-merge"); assert.strictEqual(chatSpan.attributes["user_id"], "user-merge"); // Custom property should have prefix From a2809974e8fd8b6d4b2d284ee13174d6b371cb38 Mon Sep 17 00:00:00 2001 From: nina-kollman <59646487+nina-kollman@users.noreply.github.com> Date: Sun, 21 Dec 2025 11:28:07 +0200 Subject: [PATCH 24/24] pretty --- packages/traceloop-sdk/test/associations.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts index 4c2c971b..ed2a0db0 100644 --- a/packages/traceloop-sdk/test/associations.test.ts +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -302,16 +302,10 @@ describe("Test Associations API", () => { ); assert.strictEqual(workflowSpan.attributes["user_id"], "user-propagate"); - assert.strictEqual( - taskSpan.attributes["session_id"], - "session-propagate", - ); + assert.strictEqual(taskSpan.attributes["session_id"], "session-propagate"); assert.strictEqual(taskSpan.attributes["user_id"], "user-propagate"); - assert.strictEqual( - chatSpan.attributes["session_id"], - "session-propagate", - ); + assert.strictEqual(chatSpan.attributes["session_id"], "session-propagate"); assert.strictEqual(chatSpan.attributes["user_id"], "user-propagate"); });