|
3 | 3 | */ |
4 | 4 | import { captureException } from '../../exports'; |
5 | 5 | import { getClient } from '../../currentScopes'; |
6 | | -import type { Span } from '../../types-hoist/span'; |
| 6 | +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; |
| 7 | +import type { Span, SpanAttributeValue } from '../../types-hoist/span'; |
7 | 8 | import { isThenable } from '../../utils/is'; |
8 | 9 | import { |
| 10 | + GEN_AI_CONVERSATION_ID_ATTRIBUTE, |
| 11 | + GEN_AI_OPERATION_NAME_ATTRIBUTE, |
| 12 | + GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, |
| 13 | + GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, |
| 14 | + GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE, |
| 15 | + GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, |
| 16 | + GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, |
| 17 | + GEN_AI_REQUEST_MODEL_ATTRIBUTE, |
| 18 | + GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, |
| 19 | + GEN_AI_REQUEST_STREAM_ATTRIBUTE, |
| 20 | + GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, |
| 21 | + GEN_AI_REQUEST_TOP_K_ATTRIBUTE, |
| 22 | + GEN_AI_REQUEST_TOP_P_ATTRIBUTE, |
| 23 | + GEN_AI_SYSTEM_ATTRIBUTE, |
9 | 24 | GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, |
10 | 25 | GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, |
11 | 26 | GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, |
@@ -55,6 +70,152 @@ export function buildMethodPath(currentPath: string, prop: string): string { |
55 | 70 | return currentPath ? `${currentPath}.${prop}` : prop; |
56 | 71 | } |
57 | 72 |
|
| 73 | +/** |
| 74 | + * Extract model from params or context. |
| 75 | + * params.model covers OpenAI/Anthropic, context.model/modelVersion covers Google GenAI chat instances. |
| 76 | + */ |
| 77 | +export function extractModel(params: Record<string, unknown> | undefined, context?: unknown): string { |
| 78 | + if (params && 'model' in params && typeof params.model === 'string') { |
| 79 | + return params.model; |
| 80 | + } |
| 81 | + // Google GenAI chat instances store the model on the context object |
| 82 | + if (context && typeof context === 'object') { |
| 83 | + const ctx = context as Record<string, unknown>; |
| 84 | + if (typeof ctx.model === 'string') return ctx.model; |
| 85 | + if (typeof ctx.modelVersion === 'string') return ctx.modelVersion; |
| 86 | + } |
| 87 | + return 'unknown'; |
| 88 | +} |
| 89 | + |
| 90 | +/** |
| 91 | + * Set an attribute if the key exists in the source object. |
| 92 | + */ |
| 93 | +function extractIfPresent( |
| 94 | + attributes: Record<string, SpanAttributeValue>, |
| 95 | + source: Record<string, unknown>, |
| 96 | + key: string, |
| 97 | + attribute: string, |
| 98 | +): void { |
| 99 | + if (key in source) { |
| 100 | + attributes[attribute] = source[key] as SpanAttributeValue; |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +/** |
| 105 | + * Extract available tools from request parameters. |
| 106 | + * Handles OpenAI (params.tools + web_search_options), Anthropic (params.tools), |
| 107 | + * and Google GenAI (config.tools[].functionDeclarations). |
| 108 | + */ |
| 109 | +function extractTools(params: Record<string, unknown>, config: Record<string, unknown>): string | undefined { |
| 110 | + // OpenAI: web_search_options are treated as tools |
| 111 | + const hasWebSearchOptions = params.web_search_options && typeof params.web_search_options === 'object'; |
| 112 | + const webSearchOptions = hasWebSearchOptions |
| 113 | + ? [{ type: 'web_search_options', ...(params.web_search_options as Record<string, unknown>) }] |
| 114 | + : []; |
| 115 | + |
| 116 | + // Google GenAI: tools contain functionDeclarations |
| 117 | + if ('tools' in config && Array.isArray(config.tools)) { |
| 118 | + const hasDeclarations = config.tools.some( |
| 119 | + (tool: unknown) => tool && typeof tool === 'object' && 'functionDeclarations' in (tool as Record<string, unknown>), |
| 120 | + ); |
| 121 | + if (hasDeclarations) { |
| 122 | + const declarations = (config.tools as Array<{ functionDeclarations?: unknown[] }>).flatMap( |
| 123 | + tool => tool.functionDeclarations ?? [], |
| 124 | + ); |
| 125 | + if (declarations.length > 0) { |
| 126 | + return JSON.stringify(declarations); |
| 127 | + } |
| 128 | + return undefined; |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + // OpenAI / Anthropic: tools are at the top level |
| 133 | + const tools = Array.isArray(params.tools) ? params.tools : []; |
| 134 | + const availableTools = [...tools, ...webSearchOptions]; |
| 135 | + |
| 136 | + if (availableTools.length === 0) { |
| 137 | + return undefined; |
| 138 | + } |
| 139 | + |
| 140 | + return JSON.stringify(availableTools); |
| 141 | +} |
| 142 | + |
| 143 | +/** |
| 144 | + * Extract conversation ID from request parameters. |
| 145 | + * Supports OpenAI Conversations API and previous_response_id chaining. |
| 146 | + */ |
| 147 | +function extractConversationId(params: Record<string, unknown>): string | undefined { |
| 148 | + if ('conversation' in params && typeof params.conversation === 'string') { |
| 149 | + return params.conversation; |
| 150 | + } |
| 151 | + if ('previous_response_id' in params && typeof params.previous_response_id === 'string') { |
| 152 | + return params.previous_response_id; |
| 153 | + } |
| 154 | + return undefined; |
| 155 | +} |
| 156 | + |
| 157 | +/** |
| 158 | + * Extract request attributes from AI method arguments. |
| 159 | + * Shared across all AI provider integrations (OpenAI, Anthropic, Google GenAI). |
| 160 | + */ |
| 161 | +export function extractRequestAttributes( |
| 162 | + system: string, |
| 163 | + origin: string, |
| 164 | + operationName: string, |
| 165 | + args: unknown[], |
| 166 | + context?: unknown, |
| 167 | +): Record<string, SpanAttributeValue> { |
| 168 | + const params = args.length > 0 && typeof args[0] === 'object' && args[0] !== null |
| 169 | + ? (args[0] as Record<string, unknown>) |
| 170 | + : undefined; |
| 171 | + |
| 172 | + const attributes: Record<string, SpanAttributeValue> = { |
| 173 | + [GEN_AI_SYSTEM_ATTRIBUTE]: system, |
| 174 | + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: operationName, |
| 175 | + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, |
| 176 | + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: extractModel(params, context), |
| 177 | + }; |
| 178 | + |
| 179 | + if (!params) { |
| 180 | + return attributes; |
| 181 | + } |
| 182 | + |
| 183 | + // Google GenAI nests generation params under config; OpenAI/Anthropic are flat |
| 184 | + const config = ('config' in params && typeof params.config === 'object' && params.config) |
| 185 | + ? (params.config as Record<string, unknown>) |
| 186 | + : params; |
| 187 | + |
| 188 | + // Generation parameters — handles both snake_case (OpenAI/Anthropic) and camelCase (Google GenAI) |
| 189 | + extractIfPresent(attributes, config, 'temperature', GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE); |
| 190 | + extractIfPresent(attributes, config, 'top_p', GEN_AI_REQUEST_TOP_P_ATTRIBUTE); |
| 191 | + extractIfPresent(attributes, config, 'topP', GEN_AI_REQUEST_TOP_P_ATTRIBUTE); |
| 192 | + extractIfPresent(attributes, config, 'top_k', GEN_AI_REQUEST_TOP_K_ATTRIBUTE); |
| 193 | + extractIfPresent(attributes, config, 'topK', GEN_AI_REQUEST_TOP_K_ATTRIBUTE); |
| 194 | + extractIfPresent(attributes, config, 'frequency_penalty', GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE); |
| 195 | + extractIfPresent(attributes, config, 'frequencyPenalty', GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE); |
| 196 | + extractIfPresent(attributes, config, 'presence_penalty', GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE); |
| 197 | + extractIfPresent(attributes, config, 'presencePenalty', GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE); |
| 198 | + extractIfPresent(attributes, config, 'max_tokens', GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE); |
| 199 | + extractIfPresent(attributes, config, 'maxOutputTokens', GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE); |
| 200 | + extractIfPresent(attributes, params, 'stream', GEN_AI_REQUEST_STREAM_ATTRIBUTE); |
| 201 | + extractIfPresent(attributes, params, 'encoding_format', GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE); |
| 202 | + extractIfPresent(attributes, params, 'dimensions', GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE); |
| 203 | + |
| 204 | + // Tools |
| 205 | + const tools = extractTools(params, config); |
| 206 | + if (tools) { |
| 207 | + attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = tools; |
| 208 | + } |
| 209 | + |
| 210 | + // Conversation ID (OpenAI) |
| 211 | + const conversationId = extractConversationId(params); |
| 212 | + if (conversationId) { |
| 213 | + attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] = conversationId; |
| 214 | + } |
| 215 | + |
| 216 | + return attributes; |
| 217 | +} |
| 218 | + |
58 | 219 | /** |
59 | 220 | * Set token usage attributes |
60 | 221 | * @param span - The span to add attributes to |
|
0 commit comments