diff --git a/packages/core/src/tracing/ai/gen-ai-attributes.ts b/packages/core/src/tracing/ai/gen-ai-attributes.ts index cae9a9353da9..5c740142b4f0 100644 --- a/packages/core/src/tracing/ai/gen-ai-attributes.ts +++ b/packages/core/src/tracing/ai/gen-ai-attributes.ts @@ -312,19 +312,6 @@ export const OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE = 'openai.usage.completion */ export const OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE = 'openai.usage.prompt_tokens'; -// ============================================================================= -// OPENAI OPERATIONS -// ============================================================================= - -/** - * OpenAI API operations following OpenTelemetry semantic conventions - * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#llm-request-spans - */ -export const OPENAI_OPERATIONS = { - CHAT: 'chat', - EMBEDDINGS: 'embeddings', -} as const; - // ============================================================================= // ANTHROPIC AI OPERATIONS // ============================================================================= diff --git a/packages/core/src/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index 38e4d831db3f..a454e5803c63 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -34,35 +34,34 @@ export function resolveAIRecordingOptions(options? * Maps AI method paths to OpenTelemetry semantic convention operation names * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#llm-request-spans */ -export function getFinalOperationName(methodPath: string): string { - if (methodPath.includes('messages')) { +export function getOperationName(methodPath: string): string { + // OpenAI: chat.completions.create, responses.create, conversations.create + // Anthropic: messages.create, messages.stream, completions.create + // Google GenAI: chats.create, chat.sendMessage, chat.sendMessageStream + if ( + methodPath.includes('completions') || + methodPath.includes('responses') || + methodPath.includes('conversations') || + methodPath.includes('messages') || + methodPath.includes('chat') + ) { return 'chat'; } - if (methodPath.includes('completions')) { - return 'text_completion'; + // OpenAI: embeddings.create + if (methodPath.includes('embeddings')) { + return 'embeddings'; } - // Google GenAI: models.generateContent* -> generate_content (actually generates AI responses) + // Google GenAI: models.generateContent, models.generateContentStream (must be before 'models' check) if (methodPath.includes('generateContent')) { return 'generate_content'; } - // Anthropic: models.get/retrieve -> models (metadata retrieval only) + // Anthropic: models.get, models.retrieve (metadata retrieval only) if (methodPath.includes('models')) { return 'models'; } - if (methodPath.includes('chat')) { - return 'chat'; - } return methodPath.split('.').pop() || 'unknown'; } -/** - * Get the span operation for AI methods - * Following Sentry's convention: "gen_ai.{operation_name}" - */ -export function getSpanOperation(methodPath: string): string { - return `gen_ai.${getFinalOperationName(methodPath)}`; -} - /** * Build method path from current traversal */ diff --git a/packages/core/src/tracing/anthropic-ai/index.ts b/packages/core/src/tracing/anthropic-ai/index.ts index 693ecbd23ff8..6217a08f3ffc 100644 --- a/packages/core/src/tracing/anthropic-ai/index.ts +++ b/packages/core/src/tracing/anthropic-ai/index.ts @@ -23,8 +23,7 @@ import { } from '../ai/gen-ai-attributes'; import { buildMethodPath, - getFinalOperationName, - getSpanOperation, + getOperationName, resolveAIRecordingOptions, setTokenUsageAttributes, wrapPromiseWithMethods, @@ -45,7 +44,7 @@ import { handleResponseError, messagesFromParams, setMessagesAttribute, shouldIn function extractRequestAttributes(args: unknown[], methodPath: string): Record { const attributes: Record = { [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath), + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getOperationName(methodPath), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', }; @@ -212,7 +211,7 @@ function handleStreamingRequest( const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown'; const spanConfig = { name: `${operationName} ${model}`, - op: getSpanOperation(methodPath), + op: `gen_ai.${operationName}`, attributes: requestAttributes as Record, }; @@ -272,7 +271,7 @@ function instrumentMethod( apply(target, thisArg, args: T): R | Promise { const requestAttributes = extractRequestAttributes(args, methodPath); const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown'; - const operationName = getFinalOperationName(methodPath); + const operationName = getOperationName(methodPath); const params = typeof args[0] === 'object' ? (args[0] as Record) : undefined; const isStreamRequested = Boolean(params?.stream); @@ -299,7 +298,7 @@ function instrumentMethod( const instrumentedPromise = startSpan( { name: `${operationName} ${model}`, - op: getSpanOperation(methodPath), + op: `gen_ai.${operationName}`, attributes: requestAttributes as Record, }, span => { diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index e53b320a8503..6170163a626b 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -26,13 +26,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import { truncateGenAiMessages } from '../ai/messageTruncation'; -import { - buildMethodPath, - extractSystemInstructions, - getFinalOperationName, - getSpanOperation, - resolveAIRecordingOptions, -} from '../ai/utils'; +import { buildMethodPath, extractSystemInstructions, getOperationName, resolveAIRecordingOptions } from '../ai/utils'; import { CHAT_PATH, CHATS_CREATE_METHOD, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { @@ -111,7 +105,7 @@ function extractRequestAttributes( ): Record { const attributes: Record = { [GEN_AI_SYSTEM_ATTRIBUTE]: GOOGLE_GENAI_SYSTEM_NAME, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath), + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getOperationName(methodPath), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', }; @@ -268,7 +262,7 @@ function instrumentMethod( const params = args[0] as Record | undefined; const requestAttributes = extractRequestAttributes(methodPath, params, context); const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown'; - const operationName = getFinalOperationName(methodPath); + const operationName = getOperationName(methodPath); // Check if this is a streaming method if (isStreamingMethod(methodPath)) { @@ -276,7 +270,7 @@ function instrumentMethod( return startSpanManual( { name: `${operationName} ${model}`, - op: getSpanOperation(methodPath), + op: `gen_ai.${operationName}`, attributes: requestAttributes, }, async (span: Span) => { @@ -305,7 +299,7 @@ function instrumentMethod( return startSpan( { name: isSyncCreate ? `${operationName} ${model} create` : `${operationName} ${model}`, - op: getSpanOperation(methodPath), + op: `gen_ai.${operationName}`, attributes: requestAttributes, }, (span: Span) => { diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index d5ee3f53af86..fc8e67115e87 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -15,10 +15,10 @@ import { GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, - OPENAI_OPERATIONS, } from '../ai/gen-ai-attributes'; import { extractSystemInstructions, + getOperationName, getTruncatedJsonString, resolveAIRecordingOptions, wrapPromiseWithMethods, @@ -39,8 +39,6 @@ import { addEmbeddingsAttributes, addResponsesApiAttributes, extractRequestParameters, - getOperationName, - getSpanOperation, isChatCompletionResponse, isConversationResponse, isEmbeddingsResponse, @@ -127,7 +125,7 @@ function addResponseAttributes(span: Span, result: unknown, recordOutputs?: bool // Extract and record AI request inputs, if present. This is intentionally separate from response attributes. function addRequestAttributes(span: Span, params: Record, operationName: string): void { // Store embeddings input on a separate attribute and do not truncate it - if (operationName === OPENAI_OPERATIONS.EMBEDDINGS && 'input' in params) { + if (operationName === 'embeddings' && 'input' in params) { const input = params.input; // No input provided @@ -197,7 +195,7 @@ function instrumentMethod( const spanConfig = { name: `${operationName} ${model}`, - op: getSpanOperation(methodPath), + op: `gen_ai.${operationName}`, attributes: requestAttributes as Record, }; diff --git a/packages/core/src/tracing/openai/utils.ts b/packages/core/src/tracing/openai/utils.ts index f89b786b5a3c..2ccaaeb3264a 100644 --- a/packages/core/src/tracing/openai/utils.ts +++ b/packages/core/src/tracing/openai/utils.ts @@ -16,7 +16,6 @@ import { GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, - OPENAI_OPERATIONS, OPENAI_RESPONSE_ID_ATTRIBUTE, OPENAI_RESPONSE_MODEL_ATTRIBUTE, OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE, @@ -34,34 +33,6 @@ import type { ResponseStreamingEvent, } from './types'; -/** - * Maps OpenAI method paths to OpenTelemetry semantic convention operation names - * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#llm-request-spans - */ -export function getOperationName(methodPath: string): string { - if (methodPath.includes('chat.completions')) { - return OPENAI_OPERATIONS.CHAT; - } - if (methodPath.includes('responses')) { - return OPENAI_OPERATIONS.CHAT; - } - if (methodPath.includes('embeddings')) { - return OPENAI_OPERATIONS.EMBEDDINGS; - } - if (methodPath.includes('conversations')) { - return OPENAI_OPERATIONS.CHAT; - } - return methodPath.split('.').pop() || 'unknown'; -} - -/** - * Get the span operation for OpenAI methods - * Following Sentry's convention: "gen_ai.{operation_name}" - */ -export function getSpanOperation(methodPath: string): string { - return `gen_ai.${getOperationName(methodPath)}`; -} - /** * Check if a method path should be instrumented */ diff --git a/packages/core/test/lib/utils/openai-utils.test.ts b/packages/core/test/lib/utils/openai-utils.test.ts index 65a55bcc9ef6..ca0aa08ca0f7 100644 --- a/packages/core/test/lib/utils/openai-utils.test.ts +++ b/packages/core/test/lib/utils/openai-utils.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildMethodPath } from '../../../src/tracing/ai/utils'; +import { buildMethodPath, getOperationName } from '../../../src/tracing/ai/utils'; import { - getOperationName, - getSpanOperation, isChatCompletionChunk, isChatCompletionResponse, isConversationResponse, @@ -38,14 +36,6 @@ describe('openai-utils', () => { }); }); - describe('getSpanOperation', () => { - it('should prefix operation with gen_ai', () => { - expect(getSpanOperation('chat.completions.create')).toBe('gen_ai.chat'); - expect(getSpanOperation('responses.create')).toBe('gen_ai.chat'); - expect(getSpanOperation('some.custom.operation')).toBe('gen_ai.operation'); - }); - }); - describe('shouldInstrument', () => { it('should return true for instrumented methods', () => { expect(shouldInstrument('responses.create')).toBe(true);