diff --git a/packages/core/src/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index 601807cc194d..a8bae14cb21e 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -3,15 +3,30 @@ */ import { captureException } from '../../exports'; import { getClient } from '../../currentScopes'; -import type { Span } from '../../types-hoist/span'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; +import type { Span, SpanAttributeValue } from '../../types-hoist/span'; import { isThenable } from '../../utils/is'; import { + GEN_AI_CONVERSATION_ID_ATTRIBUTE, + GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, + GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, + GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE, + GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, + GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, + GEN_AI_REQUEST_MODEL_ATTRIBUTE, + GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, + GEN_AI_REQUEST_STREAM_ATTRIBUTE, + GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, + GEN_AI_REQUEST_TOP_K_ATTRIBUTE, + GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_STREAMING_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, + GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, @@ -63,6 +78,155 @@ export function buildMethodPath(currentPath: string, prop: string): string { return currentPath ? `${currentPath}.${prop}` : prop; } +/** + * Extract model from params or context. + * params.model covers OpenAI/Anthropic, context.model/modelVersion covers Google GenAI chat instances. + */ +export function extractModel(params: Record | undefined, context?: unknown): string { + if (params && 'model' in params && typeof params.model === 'string') { + return params.model; + } + // Google GenAI chat instances store the model on the context object + if (context && typeof context === 'object') { + const ctx = context as Record; + if (typeof ctx.model === 'string') return ctx.model; + if (typeof ctx.modelVersion === 'string') return ctx.modelVersion; + } + return 'unknown'; +} + +/** + * Set an attribute if the key exists in the source object. + */ +function extractIfPresent( + attributes: Record, + source: Record, + key: string, + attribute: string, +): void { + if (key in source) { + attributes[attribute] = source[key] as SpanAttributeValue; + } +} + +/** + * Extract available tools from request parameters. + * Handles OpenAI (params.tools + web_search_options), Anthropic (params.tools), + * and Google GenAI (config.tools[].functionDeclarations). + */ +function extractTools(params: Record, config: Record): string | undefined { + // OpenAI: web_search_options are treated as tools + const hasWebSearchOptions = params.web_search_options && typeof params.web_search_options === 'object'; + const webSearchOptions = hasWebSearchOptions + ? [{ type: 'web_search_options', ...(params.web_search_options as Record) }] + : []; + + // Google GenAI: tools contain functionDeclarations + if ('tools' in config && Array.isArray(config.tools)) { + const hasDeclarations = config.tools.some( + (tool: unknown) => + tool && typeof tool === 'object' && 'functionDeclarations' in (tool as Record), + ); + if (hasDeclarations) { + const declarations = (config.tools as Array<{ functionDeclarations?: unknown[] }>).flatMap( + tool => tool.functionDeclarations ?? [], + ); + if (declarations.length > 0) { + return JSON.stringify(declarations); + } + return undefined; + } + } + + // OpenAI / Anthropic: tools are at the top level + const tools = Array.isArray(params.tools) ? params.tools : []; + const availableTools = [...tools, ...webSearchOptions]; + + if (availableTools.length === 0) { + return undefined; + } + + return JSON.stringify(availableTools); +} + +/** + * Extract conversation ID from request parameters. + * Supports OpenAI Conversations API and previous_response_id chaining. + */ +function extractConversationId(params: Record): string | undefined { + if ('conversation' in params && typeof params.conversation === 'string') { + return params.conversation; + } + if ('previous_response_id' in params && typeof params.previous_response_id === 'string') { + return params.previous_response_id; + } + return undefined; +} + +/** + * Extract request attributes from AI method arguments. + * Shared across all AI provider integrations (OpenAI, Anthropic, Google GenAI). + */ +export function extractRequestAttributes( + system: string, + origin: string, + operationName: string, + args: unknown[], + context?: unknown, +): Record { + const params = + args.length > 0 && typeof args[0] === 'object' && args[0] !== null + ? (args[0] as Record) + : undefined; + + const attributes: Record = { + [GEN_AI_SYSTEM_ATTRIBUTE]: system, + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: operationName, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: extractModel(params, context), + }; + + if (!params) { + return attributes; + } + + // Google GenAI nests generation params under config; OpenAI/Anthropic are flat + const config = + 'config' in params && typeof params.config === 'object' && params.config + ? (params.config as Record) + : params; + + // Generation parameters — handles both snake_case (OpenAI/Anthropic) and camelCase (Google GenAI) + extractIfPresent(attributes, config, 'temperature', GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE); + extractIfPresent(attributes, config, 'top_p', GEN_AI_REQUEST_TOP_P_ATTRIBUTE); + extractIfPresent(attributes, config, 'topP', GEN_AI_REQUEST_TOP_P_ATTRIBUTE); + extractIfPresent(attributes, config, 'top_k', GEN_AI_REQUEST_TOP_K_ATTRIBUTE); + extractIfPresent(attributes, config, 'topK', GEN_AI_REQUEST_TOP_K_ATTRIBUTE); + extractIfPresent(attributes, config, 'frequency_penalty', GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE); + extractIfPresent(attributes, config, 'frequencyPenalty', GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE); + extractIfPresent(attributes, config, 'presence_penalty', GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE); + extractIfPresent(attributes, config, 'presencePenalty', GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE); + extractIfPresent(attributes, config, 'max_tokens', GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE); + extractIfPresent(attributes, config, 'maxOutputTokens', GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE); + extractIfPresent(attributes, params, 'stream', GEN_AI_REQUEST_STREAM_ATTRIBUTE); + extractIfPresent(attributes, params, 'encoding_format', GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE); + extractIfPresent(attributes, params, 'dimensions', GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE); + + // Tools + const tools = extractTools(params, config); + if (tools) { + attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = tools; + } + + // Conversation ID (OpenAI) + const conversationId = extractConversationId(params); + if (conversationId) { + attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] = conversationId; + } + + return attributes; +} + /** * Set token usage attributes * @param span - The span to add attributes to diff --git a/packages/core/src/tracing/anthropic-ai/index.ts b/packages/core/src/tracing/anthropic-ai/index.ts index 14cc44d6d6be..ef216cd53658 100644 --- a/packages/core/src/tracing/anthropic-ai/index.ts +++ b/packages/core/src/tracing/anthropic-ai/index.ts @@ -1,28 +1,19 @@ import { captureException } from '../../exports'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpan, startSpanManual } from '../../tracing/trace'; import type { Span, SpanAttributeValue } from '../../types-hoist/span'; import { - GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_PROMPT_ATTRIBUTE, - GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, - GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_REQUEST_STREAM_ATTRIBUTE, - GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, - GEN_AI_REQUEST_TOP_K_ATTRIBUTE, - GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, - GEN_AI_SYSTEM_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; import { buildMethodPath, + extractRequestAttributes, resolveAIRecordingOptions, setTokenUsageAttributes, wrapPromiseWithMethods, @@ -32,42 +23,6 @@ import { instrumentAsyncIterableStream, instrumentMessageStream } from './stream import type { AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types'; import { handleResponseError, messagesFromParams, setMessagesAttribute } from './utils'; -/** - * Extract request attributes from method arguments - */ -function extractRequestAttributes(args: unknown[], methodPath: string, operationName: string): Record { - const attributes: Record = { - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - }; - - if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { - const params = args[0] as Record; - if (params.tools && Array.isArray(params.tools)) { - attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = JSON.stringify(params.tools); - } - - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = params.model ?? 'unknown'; - if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = params.temperature; - if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = params.top_p; - if ('stream' in params) attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] = params.stream; - if ('top_k' in params) attributes[GEN_AI_REQUEST_TOP_K_ATTRIBUTE] = params.top_k; - if ('frequency_penalty' in params) - attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = params.frequency_penalty; - if ('max_tokens' in params) attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE] = params.max_tokens; - } else { - if (methodPath === 'models.retrieve' || methodPath === 'models.get') { - // models.retrieve(model-id) and models.get(model-id) - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = args[0]; - } else { - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = 'unknown'; - } - } - - return attributes; -} - /** * Add private request attributes to spans. * This is only recorded if recordInputs is true. @@ -183,7 +138,7 @@ function handleStreamingRequest( target: (...args: T) => R | Promise, context: unknown, args: T, - requestAttributes: Record, + requestAttributes: Record, operationName: string, methodPath: string, params: Record | undefined, @@ -195,7 +150,7 @@ function handleStreamingRequest( const spanConfig = { name: `${operationName} ${model}`, op: `gen_ai.${operationName}`, - attributes: requestAttributes as Record, + attributes: requestAttributes, }; // messages.stream() always returns a sync MessageStream, even with stream: true param @@ -254,7 +209,13 @@ function instrumentMethod( return new Proxy(originalMethod, { apply(target, thisArg, args: T): R | Promise { const operationName = instrumentedMethod.operation || 'unknown'; - const requestAttributes = extractRequestAttributes(args, methodPath, operationName); + const requestAttributes = extractRequestAttributes('anthropic', 'auto.ai.anthropic', operationName, args); + + // Anthropic models.retrieve/models.get take model ID as positional string arg + if ((methodPath === 'models.retrieve' || methodPath === 'models.get') && typeof args[0] === 'string') { + requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = args[0]; + } + const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown'; const params = typeof args[0] === 'object' ? (args[0] as Record) : undefined; @@ -283,7 +244,7 @@ function instrumentMethod( { name: `${operationName} ${model}`, op: `gen_ai.${operationName}`, - attributes: requestAttributes as Record, + attributes: requestAttributes, }, span => { originalResult = target.apply(context, args) as Promise; diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index ac06d39a5784..ed622603461b 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -1,27 +1,17 @@ /* eslint-disable max-lines */ import { captureException } from '../../exports'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpan, startSpanManual } from '../../tracing/trace'; -import type { Span, SpanAttributeValue } from '../../types-hoist/span'; +import type { Span } from '../../types-hoist/span'; import { handleCallbackErrors } from '../../utils/handleCallbackErrors'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - GEN_AI_OPERATION_NAME_ATTRIBUTE, - GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, - GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, - GEN_AI_REQUEST_TOP_K_ATTRIBUTE, - GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, - GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, @@ -29,106 +19,18 @@ import { } from '../ai/gen-ai-attributes'; import { truncateGenAiMessages } from '../ai/messageTruncation'; import type { InstrumentedMethodEntry } from '../ai/utils'; -import { buildMethodPath, extractSystemInstructions, resolveAIRecordingOptions } from '../ai/utils'; +import { + buildMethodPath, + extractRequestAttributes, + extractSystemInstructions, + resolveAIRecordingOptions, +} from '../ai/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; import type { ContentListUnion, ContentUnion, Message, PartListUnion } from './utils'; import { contentUnionToMessages } from './utils'; -/** - * Extract model from parameters or chat context object - * For chat instances, the model is available on the chat object as 'model' (older versions) or 'modelVersion' (newer versions) - */ -export function extractModel(params: Record, context?: unknown): string { - if ('model' in params && typeof params.model === 'string') { - return params.model; - } - - // Try to get model from chat context object (chat instance has model property) - if (context && typeof context === 'object') { - const contextObj = context as Record; - - // Check for 'model' property (older versions, and streaming) - if ('model' in contextObj && typeof contextObj.model === 'string') { - return contextObj.model; - } - - // Check for 'modelVersion' property (newer versions) - if ('modelVersion' in contextObj && typeof contextObj.modelVersion === 'string') { - return contextObj.modelVersion; - } - } - - return 'unknown'; -} - -/** - * Extract generation config parameters - */ -function extractConfigAttributes(config: Record): Record { - const attributes: Record = {}; - - if ('temperature' in config && typeof config.temperature === 'number') { - attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = config.temperature; - } - if ('topP' in config && typeof config.topP === 'number') { - attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = config.topP; - } - if ('topK' in config && typeof config.topK === 'number') { - attributes[GEN_AI_REQUEST_TOP_K_ATTRIBUTE] = config.topK; - } - if ('maxOutputTokens' in config && typeof config.maxOutputTokens === 'number') { - attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE] = config.maxOutputTokens; - } - if ('frequencyPenalty' in config && typeof config.frequencyPenalty === 'number') { - attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = config.frequencyPenalty; - } - if ('presencePenalty' in config && typeof config.presencePenalty === 'number') { - attributes[GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE] = config.presencePenalty; - } - - return attributes; -} - -/** - * Extract request attributes from method arguments - * Builds the base attributes for span creation including system info, model, and config - */ -function extractRequestAttributes( - operationName: string, - params?: Record, - context?: unknown, -): Record { - const attributes: Record = { - [GEN_AI_SYSTEM_ATTRIBUTE]: GOOGLE_GENAI_SYSTEM_NAME, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - }; - - if (params) { - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel(params, context); - - // Extract generation config parameters - if ('config' in params && typeof params.config === 'object' && params.config) { - const config = params.config as Record; - Object.assign(attributes, extractConfigAttributes(config)); - - // Extract available tools from config - if ('tools' in config && Array.isArray(config.tools)) { - const functionDeclarations = config.tools.flatMap( - (tool: { functionDeclarations: unknown[] }) => tool.functionDeclarations, - ); - attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = JSON.stringify(functionDeclarations); - } - } - } else { - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel({}, context); - } - - return attributes; -} - /** * Add private request attributes to spans. * This is only recorded if recordInputs is true. @@ -270,7 +172,13 @@ function instrumentMethod( apply(target, _, args: T): R | Promise { const operationName = instrumentedMethod.operation || 'unknown'; const params = args[0] as Record | undefined; - const requestAttributes = extractRequestAttributes(operationName, params, context); + const requestAttributes = extractRequestAttributes( + GOOGLE_GENAI_SYSTEM_NAME, + 'auto.ai.google_genai', + operationName, + args, + context, + ); const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown'; // Check if this is a streaming method diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index dc728cbe806f..36b0ca8f6097 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -1,23 +1,18 @@ -import { DEBUG_BUILD } from '../../debug-build'; import { captureException } from '../../exports'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpan, startSpanManual } from '../../tracing/trace'; -import type { Span, SpanAttributeValue } from '../../types-hoist/span'; -import { debug } from '../../utils/debug-logger'; +import type { Span } from '../../types-hoist/span'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - GEN_AI_OPERATION_NAME_ATTRIBUTE, - GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../ai/utils'; import { buildMethodPath, + extractRequestAttributes, extractSystemInstructions, getTruncatedJsonString, resolveAIRecordingOptions, @@ -26,56 +21,7 @@ import { import { OPENAI_METHOD_REGISTRY } from './constants'; import { instrumentStream } from './streaming'; import type { ChatCompletionChunk, OpenAiOptions, OpenAIStream, ResponseStreamingEvent } from './types'; -import { addResponseAttributes, extractRequestParameters } from './utils'; - -/** - * Extract available tools from request parameters - */ -function extractAvailableTools(params: Record): string | undefined { - const tools = Array.isArray(params.tools) ? params.tools : []; - const hasWebSearchOptions = params.web_search_options && typeof params.web_search_options === 'object'; - const webSearchOptions = hasWebSearchOptions - ? [{ type: 'web_search_options', ...(params.web_search_options as Record) }] - : []; - - const availableTools = [...tools, ...webSearchOptions]; - if (availableTools.length === 0) { - return undefined; - } - - try { - return JSON.stringify(availableTools); - } catch (error) { - DEBUG_BUILD && debug.error('Failed to serialize OpenAI tools:', error); - return undefined; - } -} - -/** - * Extract request attributes from method arguments - */ -function extractRequestAttributes(args: unknown[], operationName: string): Record { - const attributes: Record = { - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - }; - - if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { - const params = args[0] as Record; - - const availableTools = extractAvailableTools(params); - if (availableTools) { - attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = availableTools; - } - - Object.assign(attributes, extractRequestParameters(params)); - } else { - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = 'unknown'; - } - - return attributes; -} +import { addResponseAttributes } from './utils'; // Extract and record AI request inputs, if present. This is intentionally separate from response attributes. function addRequestAttributes(span: Span, params: Record, operationName: string): void { @@ -143,7 +89,7 @@ function instrumentMethod( ): (...args: T) => Promise { return function instrumentedCall(...args: T): Promise { const operationName = instrumentedMethod.operation || 'unknown'; - const requestAttributes = extractRequestAttributes(args, operationName); + const requestAttributes = extractRequestAttributes('openai', 'auto.ai.openai', operationName, args); const model = (requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown'; const params = args[0] as Record | undefined; @@ -152,7 +98,7 @@ function instrumentMethod( const spanConfig = { name: `${operationName} ${model}`, op: `gen_ai.${operationName}`, - attributes: requestAttributes as Record, + attributes: requestAttributes, }; if (isStreamRequested) { diff --git a/packages/core/src/tracing/openai/utils.ts b/packages/core/src/tracing/openai/utils.ts index ded7b3ff0e3b..80dc50810262 100644 --- a/packages/core/src/tracing/openai/utils.ts +++ b/packages/core/src/tracing/openai/utils.ts @@ -1,15 +1,6 @@ -import type { Span } from '../../types-hoist/span'; -import type { SpanAttributeValue } from '../../types-hoist/span'; +import type { Span, SpanAttributeValue } from '../../types-hoist/span'; import { GEN_AI_CONVERSATION_ID_ATTRIBUTE, - GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, - GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE, - GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_STREAM_ATTRIBUTE, - GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, - GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, @@ -150,45 +141,3 @@ export function addResponseAttributes(span: Span, result: unknown, recordOutputs span.setAttributes(attrs); } - -/** - * Extract conversation ID from request parameters - * Supports both Conversations API and previous_response_id chaining - * @see https://platform.openai.com/docs/guides/conversation-state - */ -function extractConversationId(params: Record): string | undefined { - // Conversations API: conversation parameter (e.g., "conv_...") - if ('conversation' in params && typeof params.conversation === 'string') { - return params.conversation; - } - // Responses chaining: previous_response_id links to parent response - if ('previous_response_id' in params && typeof params.previous_response_id === 'string') { - return params.previous_response_id; - } - return undefined; -} - -/** - * Extract request parameters including model settings and conversation context - */ -export function extractRequestParameters(params: Record): Record { - const attributes: Record = { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: params.model ?? 'unknown', - }; - - if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = params.temperature; - if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = params.top_p; - if ('frequency_penalty' in params) attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = params.frequency_penalty; - if ('presence_penalty' in params) attributes[GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE] = params.presence_penalty; - if ('stream' in params) attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] = params.stream; - if ('encoding_format' in params) attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE] = params.encoding_format; - if ('dimensions' in params) attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE] = params.dimensions; - - // Capture conversation ID for linking messages across API calls - const conversationId = extractConversationId(params); - if (conversationId) { - attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] = conversationId; - } - - return attributes; -}