From f459c3435d661c4f855378a1bdbfd41a5d287f65 Mon Sep 17 00:00:00 2001 From: kashish Date: Thu, 15 Oct 2026 12:28:15 +0530 Subject: [PATCH] fix(model): omit request model attribute instead of setting 'unknown' --- .../server-utils/src/ai/anthropic-ai/index.ts | 8 +- .../server-utils/src/ai/google-genai/index.ts | 4 +- .../src/ai/langchain/embeddings.ts | 6 +- .../server-utils/src/ai/langchain/index.ts | 8 +- .../server-utils/src/ai/langchain/utils.ts | 2 +- packages/server-utils/src/ai/openai/index.ts | 124 ++++++++++++------ packages/server-utils/src/ai/openai/utils.ts | 2 +- .../server-utils/src/ai/workers-ai/index.ts | 4 +- .../server-utils/src/ai/workers-ai/utils.ts | 2 +- .../src/integrations/anthropic.ts | 4 +- .../src/integrations/google-genai.ts | 2 +- .../server-utils/src/integrations/openai.ts | 4 +- 12 files changed, 105 insertions(+), 65 deletions(-) diff --git a/packages/server-utils/src/ai/anthropic-ai/index.ts b/packages/server-utils/src/ai/anthropic-ai/index.ts index 173ff3c638d4..7563c21859a7 100644 --- a/packages/server-utils/src/ai/anthropic-ai/index.ts +++ b/packages/server-utils/src/ai/anthropic-ai/index.ts @@ -59,15 +59,13 @@ export function extractRequestAttributes( attributes[GEN_AI_TOOL_DEFINITIONS] = JSON.stringify(params.tools); } - attributes[GEN_AI_REQUEST_MODEL] = params.model ?? 'unknown'; + attributes[GEN_AI_REQUEST_MODEL] = params.model; if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE] = params.temperature; if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P] = 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] = params.top_k; if ('frequency_penalty' in params) attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY] = params.frequency_penalty; if ('max_tokens' in params) attributes[GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens; - } else { - attributes[GEN_AI_REQUEST_MODEL] = 'unknown'; } return attributes; @@ -261,12 +259,12 @@ function instrumentMethod( const operationName = instrumentedMethod.operation || 'unknown'; const requestAttributes = extractRequestAttributes(args, operationName, !!options.recordInputs); - const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown'; + const model = requestAttributes[GEN_AI_REQUEST_MODEL]; const client = getClient(); // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. const spanConfig = { name: - (typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client)) + (typeof model === 'string') || !(client && hasSpanStreamingEnabled(client)) ? `${operationName} ${model}` : operationName, op: getGenAiSpanOp(operationName), diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index 16dbb01d98f9..9e877473bf8c 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -66,8 +66,6 @@ export function extractModel(params: Record, context?: unknown) return contextObj.modelVersion; } } - - return 'unknown'; } /** @@ -314,7 +312,7 @@ function instrumentMethod( const client = getClient(); // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. const spanName = - (typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client)) + (typeof model === 'string') || !(client && hasSpanStreamingEnabled(client)) ? `${operationName} ${model}` : operationName; diff --git a/packages/server-utils/src/ai/langchain/embeddings.ts b/packages/server-utils/src/ai/langchain/embeddings.ts index e1c4fd25fd9b..8b511bd1691b 100644 --- a/packages/server-utils/src/ai/langchain/embeddings.ts +++ b/packages/server-utils/src/ai/langchain/embeddings.ts @@ -43,7 +43,7 @@ function extractEmbeddingAttributes(instance: unknown): Record [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, [SENTRY_OP]: GEN_AI_EMBEDDINGS, [GEN_AI_OPERATION_NAME]: 'embeddings', - [GEN_AI_REQUEST_MODEL]: embeddingsInstance.model ?? 'unknown', + [GEN_AI_REQUEST_MODEL]: embeddingsInstance.model, }; attributes[GEN_AI_PROVIDER_NAME] = inferSystemFromInstance(embeddingsInstance); @@ -70,7 +70,7 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions( ): { name: string; attributes: Record } { const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractEmbeddingAttributes(instance); - const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown'; + const modelName = attributes[GEN_AI_REQUEST_MODEL]; const client = getClient(); if (recordInputs && input != null) { @@ -80,7 +80,7 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions( return { // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. name: - (typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client)) + (typeof modelName === 'string') || !(client && hasSpanStreamingEnabled(client)) ? `embeddings ${modelName}` : 'embeddings', attributes: attributes as Record, diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index 11a1afb7d391..391a76408354 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -105,7 +105,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): invocationParams, metadata, ); - const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown'; + const modelName = attributes[GEN_AI_REQUEST_MODEL]; const operationName = typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown'; const client = getClient(); @@ -114,7 +114,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): { // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. name: - (typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client)) + (typeof modelName === 'string') || !(client && hasSpanStreamingEnabled(client)) ? `${operationName} ${modelName}` : operationName, attributes: { @@ -156,7 +156,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): attributes[GEN_AI_TOOL_DEFINITIONS] = toolDefsJson; } - const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown'; + const modelName = attributes[GEN_AI_REQUEST_MODEL]; const operationName = typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown'; const client = getClient(); @@ -165,7 +165,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): { // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. name: - (typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client)) + (typeof modelName === 'string') || !(client && hasSpanStreamingEnabled(client)) ? `${operationName} ${modelName}` : operationName, attributes: { diff --git a/packages/server-utils/src/ai/langchain/utils.ts b/packages/server-utils/src/ai/langchain/utils.ts index 8ae219e42437..281d662a45c7 100644 --- a/packages/server-utils/src/ai/langchain/utils.ts +++ b/packages/server-utils/src/ai/langchain/utils.ts @@ -247,7 +247,7 @@ export function extractLLMRequestAttributes( langSmithMetadata?: Record, ): Record { const system = langSmithMetadata?.ls_provider; - const modelName = invocationParams?.model ?? langSmithMetadata?.ls_model_name ?? 'unknown'; + const modelName = invocationParams?.model ?? langSmithMetadata?.ls_model_name; const attrs = baseRequestAttributes(system, modelName, llm, invocationParams, langSmithMetadata); diff --git a/packages/server-utils/src/ai/openai/index.ts b/packages/server-utils/src/ai/openai/index.ts index 981da4281ef5..4f170d53fb4d 100644 --- a/packages/server-utils/src/ai/openai/index.ts +++ b/packages/server-utils/src/ai/openai/index.ts @@ -1,5 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { DEBUG_BUILD } from '../../debug-build'; +import { DEBUG_BUILD } from "../../debug-build"; import { getClient, hasSpanStreamingEnabled, @@ -8,8 +8,8 @@ import { startSpanManual, debug, stringify, -} from '@sentry/core'; -import type { Span, SpanAttributeValue } from '@sentry/core'; +} from "@sentry/core"; +import type { Span, SpanAttributeValue } from "@sentry/core"; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -19,28 +19,41 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, SENTRY_ORIGIN, -} from '@sentry/conventions/attributes'; -import type { InstrumentedMethodEntry } from '../core/utils'; +} from "@sentry/conventions/attributes"; +import type { InstrumentedMethodEntry } from "../core/utils"; import { buildMethodPath, extractSystemInstructions, getGenAiSpanOp, resolveAIRecordingOptions, wrapPromiseWithMethods, -} from '../core/utils'; -import { OPENAI_METHOD_REGISTRY } from './constants'; -import { instrumentStream } from './streaming'; -import type { ChatCompletionChunk, OpenAiOptions, OpenAIStream, ResponseStreamingEvent } from './types'; -import { addResponseAttributes, extractRequestParameters } from './utils'; +} from "../core/utils"; +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 { +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 hasWebSearchOptions = + params.web_search_options && typeof params.web_search_options === "object"; const webSearchOptions = hasWebSearchOptions - ? [{ type: 'web_search_options', ...(params.web_search_options as Record) }] + ? [ + { + type: "web_search_options", + ...(params.web_search_options as Record), + }, + ] : []; const availableTools = [...tools, ...webSearchOptions]; @@ -51,7 +64,7 @@ function extractAvailableTools(params: Record): string | undefi try { return JSON.stringify(availableTools); } catch (error) { - DEBUG_BUILD && debug.error('Failed to serialize OpenAI tools:', error); + DEBUG_BUILD && debug.error("Failed to serialize OpenAI tools:", error); return undefined; } } @@ -65,31 +78,35 @@ export function extractRequestAttributes( recordInputs: boolean, ): Record { const attributes: Record = { - [GEN_AI_PROVIDER_NAME]: 'openai', + [GEN_AI_PROVIDER_NAME]: "openai", [GEN_AI_OPERATION_NAME]: operationName, - [SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: "auto.ai.openai", }; - if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { + if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) { const params = args[0] as Record; - const availableTools = recordInputs ? extractAvailableTools(params) : undefined; + const availableTools = recordInputs + ? extractAvailableTools(params) + : undefined; if (availableTools) { attributes[GEN_AI_TOOL_DEFINITIONS] = availableTools; } Object.assign(attributes, extractRequestParameters(params)); - } else { - attributes[GEN_AI_REQUEST_MODEL] = 'unknown'; } return attributes; } // Extract and record AI request inputs, if present. This is intentionally separate from response attributes. -export function addRequestAttributes(span: Span, params: Record, operationName: string): void { +export function addRequestAttributes( + span: Span, + params: Record, + operationName: string, +): void { // Store embeddings input on a separate attribute - if (operationName === 'embeddings' && 'input' in params) { + if (operationName === "embeddings" && "input" in params) { const input = params.input; // No input provided @@ -98,7 +115,7 @@ export function addRequestAttributes(span: Span, params: Record } // Empty input string - if (typeof input === 'string' && input.length === 0) { + if (typeof input === "string" && input.length === 0) { return; } @@ -111,7 +128,12 @@ export function addRequestAttributes(span: Span, params: Record return; } - const src = 'input' in params ? params.input : 'messages' in params ? params.messages : undefined; + const src = + "input" in params + ? params.input + : "messages" in params + ? params.messages + : undefined; if (!src) { return; @@ -121,7 +143,8 @@ export function addRequestAttributes(span: Span, params: Record return; } - const { systemInstructions, filteredMessages } = extractSystemInstructions(src); + const { systemInstructions, filteredMessages } = + extractSystemInstructions(src); if (systemInstructions) { span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, systemInstructions); @@ -143,18 +166,24 @@ function instrumentMethod( options: OpenAiOptions, ): (...args: T) => Promise { return function instrumentedCall(...args: T): Promise { - const operationName = instrumentedMethod.operation || 'unknown'; - const requestAttributes = extractRequestAttributes(args, operationName, !!options.recordInputs); - const model = (requestAttributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; + const operationName = instrumentedMethod.operation || "unknown"; + const requestAttributes = extractRequestAttributes( + args, + operationName, + !!options.recordInputs, + ); + const model = + (requestAttributes[GEN_AI_REQUEST_MODEL] as string); const params = args[0] as Record | undefined; - const isStreamRequested = params && typeof params === 'object' && params.stream === true; + const isStreamRequested = + params && typeof params === "object" && params.stream === true; const client = getClient(); const spanConfig = { // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. name: - model !== 'unknown' || !(client && hasSpanStreamingEnabled(client)) + !(client && hasSpanStreamingEnabled(client)) ? `${operationName} ${model}` : operationName, op: getGenAiSpanOp(operationName), @@ -176,12 +205,17 @@ function instrumentMethod( try { const result = await originalResult; return instrumentStream( - result as OpenAIStream, + result as OpenAIStream< + ChatCompletionChunk | ResponseStreamingEvent + >, span, options.recordOutputs ?? false, ) as unknown as R; } catch (error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + span.setStatus({ + code: SPAN_STATUS_ERROR, + message: "internal_error", + }); span.end(); throw error; } @@ -202,7 +236,7 @@ function instrumentMethod( addRequestAttributes(span, params, operationName); } - return originalResult.then(result => { + return originalResult.then((result) => { addResponseAttributes(span, result, options.recordOutputs); return result; }); @@ -215,14 +249,21 @@ function instrumentMethod( /** * Create a deep proxy for OpenAI client instrumentation */ -function createDeepProxy(target: T, currentPath = '', options: OpenAiOptions): T { +function createDeepProxy( + target: T, + currentPath = "", + options: OpenAiOptions, +): T { return new Proxy(target, { get(obj: object, prop: string): unknown { const value = (obj as Record)[prop]; const methodPath = buildMethodPath(currentPath, String(prop)); - const instrumentedMethod = OPENAI_METHOD_REGISTRY[methodPath as keyof typeof OPENAI_METHOD_REGISTRY]; - if (typeof value === 'function' && instrumentedMethod) { + const instrumentedMethod = + OPENAI_METHOD_REGISTRY[ + methodPath as keyof typeof OPENAI_METHOD_REGISTRY + ]; + if (typeof value === "function" && instrumentedMethod) { return instrumentMethod( value as (...args: unknown[]) => Promise, methodPath, @@ -232,13 +273,13 @@ function createDeepProxy(target: T, currentPath = '', options: ); } - if (typeof value === 'function') { + if (typeof value === "function") { // Bind non-instrumented functions to preserve the original `this` context, // which is required for accessing private class fields (e.g. #baseURL) in OpenAI SDK v5. return value.bind(obj); } - if (value && typeof value === 'object') { + if (value && typeof value === "object") { return createDeepProxy(value, methodPath, options); } @@ -251,6 +292,9 @@ function createDeepProxy(target: T, currentPath = '', options: * Instrument an OpenAI client with Sentry tracing * Can be used across Node.js, Cloudflare Workers, and Vercel Edge */ -export function instrumentOpenAiClient(client: T, options?: OpenAiOptions): T { - return createDeepProxy(client, '', resolveAIRecordingOptions(options)); +export function instrumentOpenAiClient( + client: T, + options?: OpenAiOptions, +): T { + return createDeepProxy(client, "", resolveAIRecordingOptions(options)); } diff --git a/packages/server-utils/src/ai/openai/utils.ts b/packages/server-utils/src/ai/openai/utils.ts index 3f6afeac1c44..7400cb91d55c 100644 --- a/packages/server-utils/src/ai/openai/utils.ts +++ b/packages/server-utils/src/ai/openai/utils.ts @@ -180,7 +180,7 @@ function extractConversationId(params: Record): string | undefi */ export function extractRequestParameters(params: Record): Record { const attributes: Record = { - [GEN_AI_REQUEST_MODEL]: params.model ?? 'unknown', + [GEN_AI_REQUEST_MODEL]: params.model, }; if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE] = params.temperature; diff --git a/packages/server-utils/src/ai/workers-ai/index.ts b/packages/server-utils/src/ai/workers-ai/index.ts index ef49c2c4b89a..56751364a47a 100644 --- a/packages/server-utils/src/ai/workers-ai/index.ts +++ b/packages/server-utils/src/ai/workers-ai/index.ts @@ -39,7 +39,7 @@ function instrumentRun( const operationName = getOperationName(model, inputs); const requestAttributes = extractRequestAttributes(model, inputs, operationName); - const modelName = typeof model === 'string' && model ? model : 'unknown'; + const modelName = typeof model === 'string'; const client = getClient(); const isStreamRequested = @@ -52,7 +52,7 @@ function instrumentRun( const spanConfig = { // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. name: - modelName !== 'unknown' || !(client && hasSpanStreamingEnabled(client)) + !(client && hasSpanStreamingEnabled(client)) ? `${operationName} ${modelName}` : operationName, attributes: { diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index 29db4d2cd136..c10a4c7c9a79 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -76,7 +76,7 @@ export function extractRequestAttributes( [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: operationName, [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, - [GEN_AI_REQUEST_MODEL]: typeof model === 'string' ? model : 'unknown', + [GEN_AI_REQUEST_MODEL]: typeof model === 'string', }; if (inputs && typeof inputs === 'object') { diff --git a/packages/server-utils/src/integrations/anthropic.ts b/packages/server-utils/src/integrations/anthropic.ts index a45bbf02496f..1ac256e370fe 100644 --- a/packages/server-utils/src/integrations/anthropic.ts +++ b/packages/server-utils/src/integrations/anthropic.ts @@ -109,13 +109,13 @@ function createGenAiSpan( const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractRequestAttributes(args, operation, recordInputs); - const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; + const model = (attributes[GEN_AI_REQUEST_MODEL] as string); attributes[SENTRY_ORIGIN] = ORIGIN; const client = getClient(); const span = startInactiveSpan({ // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. - name: model !== 'unknown' || !(client && hasSpanStreamingEnabled(client)) ? `${operation} ${model}` : operation, + name: !(client && hasSpanStreamingEnabled(client)) ? `${operation} ${model}` : operation, op: getGenAiSpanOp(operation), attributes: attributes as Record, }); diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index 846ba9ae40cf..07cf0a46106d 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -107,7 +107,7 @@ function createGenAiSpan( const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractRequestAttributes(operation, params, data.self, recordInputs); - const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; + const model = (attributes[GEN_AI_REQUEST_MODEL] as string); attributes[SENTRY_ORIGIN] = ORIGIN; const client = getClient(); diff --git a/packages/server-utils/src/integrations/openai.ts b/packages/server-utils/src/integrations/openai.ts index ec7c852e9010..8fd4a12ef3b3 100644 --- a/packages/server-utils/src/integrations/openai.ts +++ b/packages/server-utils/src/integrations/openai.ts @@ -85,12 +85,12 @@ function createGenAiSpan(data: OrchestrionChannelContext, operation: string, opt const attributes = extractRequestAttributes(args, operation, recordInputs); attributes[SENTRY_ORIGIN] = ORIGIN; - const model = (params?.model as string) || 'unknown'; + const model = (params?.model as string); const client = getClient(); const span = startInactiveSpan({ // With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality. - name: model !== 'unknown' || !(client && hasSpanStreamingEnabled(client)) ? `${operation} ${model}` : operation, + name: !(client && hasSpanStreamingEnabled(client)) ? `${operation} ${model}` : operation, op: getGenAiSpanOp(operation), attributes: attributes as Record, });