Skip to content

Commit 53a11fc

Browse files
committed
unify
1 parent 8b62c33 commit 53a11fc

File tree

5 files changed

+176
-260
lines changed

5 files changed

+176
-260
lines changed

packages/core/src/tracing/ai/utils.ts

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@
33
*/
44
import { captureException } from '../../exports';
55
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';
78
import { isThenable } from '../../utils/is';
89
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,
924
GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
1025
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
1126
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
@@ -55,6 +70,152 @@ export function buildMethodPath(currentPath: string, prop: string): string {
5570
return currentPath ? `${currentPath}.${prop}` : prop;
5671
}
5772

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+
58219
/**
59220
* Set token usage attributes
60221
* @param span - The span to add attributes to

packages/core/src/tracing/anthropic-ai/index.ts

Lines changed: 8 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
11
import { captureException } from '../../exports';
2-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
32
import { SPAN_STATUS_ERROR } from '../../tracing';
43
import { startSpan, startSpanManual } from '../../tracing/trace';
54
import type { Span, SpanAttributeValue } from '../../types-hoist/span';
65
import {
7-
GEN_AI_OPERATION_NAME_ATTRIBUTE,
86
GEN_AI_PROMPT_ATTRIBUTE,
9-
GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE,
10-
GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE,
11-
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
127
GEN_AI_REQUEST_MODEL_ATTRIBUTE,
13-
GEN_AI_REQUEST_STREAM_ATTRIBUTE,
14-
GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE,
15-
GEN_AI_REQUEST_TOP_K_ATTRIBUTE,
16-
GEN_AI_REQUEST_TOP_P_ATTRIBUTE,
178
GEN_AI_RESPONSE_ID_ATTRIBUTE,
189
GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
1910
GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
2011
GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
21-
GEN_AI_SYSTEM_ATTRIBUTE,
2212
} from '../ai/gen-ai-attributes';
2313
import type { InstrumentedMethodEntry } from '../ai/utils';
2414
import {
2515
buildMethodPath,
16+
extractRequestAttributes,
2617
resolveAIRecordingOptions,
2718
setTokenUsageAttributes,
2819
wrapPromiseWithMethods,
@@ -32,42 +23,6 @@ import { instrumentAsyncIterableStream, instrumentMessageStream } from './stream
3223
import type { AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types';
3324
import { handleResponseError, messagesFromParams, setMessagesAttribute } from './utils';
3425

35-
/**
36-
* Extract request attributes from method arguments
37-
*/
38-
function extractRequestAttributes(args: unknown[], methodPath: string, operationName: string): Record<string, unknown> {
39-
const attributes: Record<string, unknown> = {
40-
[GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic',
41-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: operationName,
42-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic',
43-
};
44-
45-
if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
46-
const params = args[0] as Record<string, unknown>;
47-
if (params.tools && Array.isArray(params.tools)) {
48-
attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = JSON.stringify(params.tools);
49-
}
50-
51-
attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = params.model ?? 'unknown';
52-
if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = params.temperature;
53-
if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = params.top_p;
54-
if ('stream' in params) attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] = params.stream;
55-
if ('top_k' in params) attributes[GEN_AI_REQUEST_TOP_K_ATTRIBUTE] = params.top_k;
56-
if ('frequency_penalty' in params)
57-
attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = params.frequency_penalty;
58-
if ('max_tokens' in params) attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE] = params.max_tokens;
59-
} else {
60-
if (methodPath === 'models.retrieve' || methodPath === 'models.get') {
61-
// models.retrieve(model-id) and models.get(model-id)
62-
attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = args[0];
63-
} else {
64-
attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = 'unknown';
65-
}
66-
}
67-
68-
return attributes;
69-
}
70-
7126
/**
7227
* Add private request attributes to spans.
7328
* This is only recorded if recordInputs is true.
@@ -254,7 +209,13 @@ function instrumentMethod<T extends unknown[], R>(
254209
return new Proxy(originalMethod, {
255210
apply(target, thisArg, args: T): R | Promise<R> {
256211
const operationName = instrumentedMethod.operation;
257-
const requestAttributes = extractRequestAttributes(args, methodPath, operationName);
212+
const requestAttributes = extractRequestAttributes('anthropic', 'auto.ai.anthropic', operationName, args);
213+
214+
// Anthropic models.retrieve/models.get take model ID as positional string arg
215+
if ((methodPath === 'models.retrieve' || methodPath === 'models.get') && typeof args[0] === 'string') {
216+
requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = args[0];
217+
}
218+
258219
const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
259220

260221
const params = typeof args[0] === 'object' ? (args[0] as Record<string, unknown>) : undefined;

0 commit comments

Comments
 (0)