1/**
2 * Adds OpenTelemetry GenAI attributes to Effect AI spans.
3 *
4 * This module models the `gen_ai.*` attributes used by language model and
5 * embedding providers. It includes attribute types, helpers for writing
6 * non-null attributes onto existing spans, and a `CurrentSpanTransformer`
7 * service for adding custom span annotations from provider options and response
8 * parts.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Context from "../Context.ts"
14import { dual } from "../Function.ts"
15import * as Predicate from "../Predicate.ts"
16import * as String from "../String.ts"
17import type * as Struct from "../Struct.ts"
18import type { Span } from "../Tracer.ts"
19import type { ProviderOptions } from "./LanguageModel.ts"
20import type * as Response from "./Response.ts"
21
22/**
23 * The attributes used to describe telemetry in the context of Generative
24 * Artificial Intelligence (GenAI) models requests and responses.
25 *
26 * **Details**
27 *
28 * These attributes follow the OpenTelemetry generative AI semantic
29 * conventions:
30 * https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/
31 *
32 * @stability unstable
33 * @category models
34 * @since 4.0.0
35 */
36export type GenAITelemetryAttributes = Struct.Simplify<
37 & AttributesWithPrefix<BaseAttributes, "gen_ai">
38 & AttributesWithPrefix<OperationAttributes, "gen_ai.operation">
39 & AttributesWithPrefix<TokenAttributes, "gen_ai.token">
40 & AttributesWithPrefix<UsageAttributes, "gen_ai.usage">
41 & AttributesWithPrefix<RequestAttributes, "gen_ai.request">
42 & AttributesWithPrefix<ResponseAttributes, "gen_ai.response">
43>
44
45/**
46 * All telemetry attributes which are part of the GenAI specification.
47 *
48 * @stability unstable
49 * @category models
50 * @since 4.0.0
51 */
52export type AllAttributes =
53 & BaseAttributes
54 & OperationAttributes
55 & TokenAttributes
56 & UsageAttributes
57 & RequestAttributes
58 & ResponseAttributes
59
60/**
61 * Telemetry attributes which are part of the GenAI specification and are
62 * namespaced by `gen_ai`.
63 *
64 * @stability unstable
65 * @category models
66 * @since 4.0.0
67 */
68export interface BaseAttributes {
69 /**
70 * The Generative AI product as identified by the client or server
71 * instrumentation.
72 */
73 readonly system?: (string & {}) | WellKnownSystem | null | undefined
74}
75
76/**
77 * Telemetry attributes which are part of the GenAI specification and are
78 * namespaced by `gen_ai.operation`.
79 *
80 * @stability unstable
81 * @category models
82 * @since 4.0.0
83 */
84export interface OperationAttributes {
85 readonly name?: (string & {}) | WellKnownOperationName | null | undefined
86}
87
88/**
89 * Telemetry attributes which are part of the GenAI specification and are
90 * namespaced by `gen_ai.token`.
91 *
92 * @stability unstable
93 * @category models
94 * @since 4.0.0
95 */
96export interface TokenAttributes {
97 readonly type?: string | null | undefined
98}
99
100/**
101 * Telemetry attributes which are part of the GenAI specification and are
102 * namespaced by `gen_ai.usage`.
103 *
104 * @stability unstable
105 * @category models
106 * @since 4.0.0
107 */
108export interface UsageAttributes {
109 readonly inputTokens?: number | null | undefined
110 readonly outputTokens?: number | null | undefined
111}
112
113/**
114 * Telemetry attributes which are part of the GenAI specification and are
115 * namespaced by `gen_ai.request`.
116 *
117 * @stability unstable
118 * @category models
119 * @since 4.0.0
120 */
121export interface RequestAttributes {
122 /**
123 * The name of the GenAI model a request is being made to.
124 */
125 readonly model?: string | null | undefined
126 /**
127 * The temperature setting for the GenAI request.
128 */
129 readonly temperature?: number | null | undefined
130 /**
131 * The temperature setting for the GenAI request.
132 */
133 readonly topK?: number | null | undefined
134 /**
135 * The top_k sampling setting for the GenAI request.
136 */
137 readonly topP?: number | null | undefined
138 /**
139 * The top_p sampling setting for the GenAI request.
140 */
141 readonly maxTokens?: number | null | undefined
142 /**
143 * The encoding formats requested in an embeddings operation, if specified.
144 */
145 readonly encodingFormats?: ReadonlyArray<string> | null | undefined
146 /**
147 * List of sequences that the model will use to stop generating further
148 * tokens.
149 */
150 readonly stopSequences?: ReadonlyArray<string> | null | undefined
151 /**
152 * The frequency penalty setting for the GenAI request.
153 */
154 readonly frequencyPenalty?: number | null | undefined
155 /**
156 * The presence penalty setting for the GenAI request.
157 */
158 readonly presencePenalty?: number | null | undefined
159 /**
160 * The seed setting for the GenAI request. Requests with same seed value
161 * are more likely to return same result.
162 */
163 readonly seed?: number | null | undefined
164}
165
166/**
167 * Telemetry attributes which are part of the GenAI specification and are
168 * namespaced by `gen_ai.response`.
169 *
170 * @stability unstable
171 * @category models
172 * @since 4.0.0
173 */
174export interface ResponseAttributes {
175 /**
176 * The unique identifier for the completion.
177 */
178 readonly id?: string | null | undefined
179 /**
180 * The name of the model that generated the response.
181 */
182 readonly model?: string | null | undefined
183 /**
184 * Array of reasons the model stopped generating tokens, corresponding to
185 * each generation received.
186 */
187 readonly finishReasons?: ReadonlyArray<string> | null | undefined
188}
189
190/**
191 * The `gen_ai.operation.name` attribute has the following list of well-known
192 * values.
193 *
194 * **Details**
195 *
196 * If one of them applies, then the respective value **MUST** be used;
197 * otherwise, a custom value **MAY** be used.
198 *
199 * @stability unstable
200 * @category models
201 * @since 4.0.0
202 */
203export type WellKnownOperationName = "chat" | "embeddings" | "text_completion"
204
205/**
206 * The `gen_ai.system` attribute has the following list of well-known values.
207 *
208 * **Details**
209 *
210 * If one of them applies, then the respective value **MUST** be used;
211 * otherwise, a custom value **MAY** be used.
212 *
213 * @stability unstable
214 * @category models
215 * @since 4.0.0
216 */
217export type WellKnownSystem =
218 | "anthropic"
219 | "aws.bedrock"
220 | "az.ai.inference"
221 | "az.ai.openai"
222 | "cohere"
223 | "deepseek"
224 | "gemini"
225 | "groq"
226 | "ibm.watsonx.ai"
227 | "mistral_ai"
228 | "openai"
229 | "perplexity"
230 | "vertex_ai"
231 | "xai"
232
233/**
234 * Utility type for prefixing attribute names with a namespace.
235 *
236 * **Details**
237 *
238 * Transforms attribute keys by adding a prefix and formatting them according to
239 * OpenTelemetry conventions (camelCase to snake_case).
240 *
241 * **Example** (Prefixing telemetry attributes)
242 *
243 * ```ts import.meta.vitest
244 * import type { Telemetry } from "effect/ai"
245 *
246 * type RequestAttrs = {
247 * modelName: string
248 * maxTokens: number
249 * }
250 *
251 * type PrefixedAttrs = Telemetry.AttributesWithPrefix<
252 * RequestAttrs,
253 * "gen_ai.request"
254 * >
255 * // Results in: {
256 * // "gen_ai.request.model_name": string
257 * // "gen_ai.request.max_tokens": number
258 * // }
259 * const attributes: PrefixedAttrs = {
260 * "gen_ai.request.model_name": "gpt-4",
261 * "gen_ai.request.max_tokens": 1000
262 * }
263 * Object.keys(attributes) // => ["gen_ai.request.model_name", "gen_ai.request.max_tokens"]
264 * ```
265 *
266 * @stability unstable
267 * @category utility types
268 * @since 4.0.0
269 */
270export type AttributesWithPrefix<Attributes extends Record<string, any>, Prefix extends string> = {
271 [Name in keyof Attributes as `${Prefix}.${FormatAttributeName<Name>}`]: Attributes[Name]
272}
273
274/**
275 * Utility type for converting camelCase names to snake_case format.
276 *
277 * **Details**
278 *
279 * This type recursively transforms string literal types from camelCase to
280 * snake_case, which is the standard format for OpenTelemetry attributes.
281 *
282 * **Example** (Formatting attribute names)
283 *
284 * ```ts import.meta.vitest
285 * import type { Telemetry } from "effect/ai"
286 *
287 * type Formatted1 = Telemetry.FormatAttributeName<"modelName"> // "model_name"
288 * type Formatted2 = Telemetry.FormatAttributeName<"maxTokens"> // "max_tokens"
289 * type Formatted3 = Telemetry.FormatAttributeName<"temperature"> // "temperature"
290 *
291 * const formatted: [Formatted1, Formatted2, Formatted3] = [
292 * "model_name",
293 * "max_tokens",
294 * "temperature"
295 * ]
296 * formatted // => ["model_name", "max_tokens", "temperature"]
297 * ```
298 *
299 * @stability unstable
300 * @category utility types
301 * @since 4.0.0
302 */
303export type FormatAttributeName<T extends string | number | symbol> = T extends string ?
304 T extends `${infer First}${infer Rest}`
305 ? `${First extends Uppercase<First> ? "_" : ""}${Lowercase<First>}${FormatAttributeName<Rest>}`
306 : T :
307 never
308
309/**
310 * Creates a reusable span-attribute writer for a key prefix and key
311 * transformer.
312 *
313 * **Details**
314 *
315 * The returned function mutates the supplied span by adding each non-nullish
316 * attribute as `${prefix}.${transformedKey}`.
317 *
318 * **Example** (Adding prefixed span attributes)
319 *
320 * ```ts import.meta.vitest
321 * import { Context, Option, String, Tracer } from "effect"
322 * import { Telemetry } from "effect/ai"
323 *
324 * const addCustomAttributes = Telemetry.addSpanAttributes(
325 * "custom.ai",
326 * String.camelToSnake
327 * )
328 *
329 * const span = new Tracer.NativeSpan({
330 * name: "request",
331 * parent: Option.none(),
332 * annotations: Context.empty(),
333 * links: [],
334 * startTime: 0n,
335 * kind: "internal",
336 * sampled: true
337 * })
338 *
339 * addCustomAttributes(span, {
340 * modelName: "gpt-4",
341 * maxTokens: 1000
342 * })
343 *
344 * Array.from(span.attributes.keys()) // => ["custom.ai.model_name", "custom.ai.max_tokens"]
345 * ```
346 *
347 * @stability unstable
348 * @category annotations
349 * @since 4.0.0
350 */
351export const addSpanAttributes = (
352 /**
353 * The prefix to add to all attribute keys.
354 */
355 keyPrefix: string,
356 /**
357 * Function to transform attribute keys (e.g., camelCase to snake_case).
358 */
359 transformKey: (key: string) => string
360) =>
361<Attributes extends Record<string, any>>(
362 /**
363 * The OpenTelemetry span to add attributes to.
364 */
365 span: Span,
366 /**
367 * The attributes to add to the span.
368 */
369 attributes: Attributes
370): void => {
371 for (const [key, value] of Object.entries(attributes)) {
372 if (Predicate.isNotNullish(value)) {
373 span.attribute(`${keyPrefix}.${transformKey(key)}`, value)
374 }
375 }
376}
377
378const addSpanBaseAttributes = addSpanAttributes("gen_ai", String.camelToSnake)<BaseAttributes>
379const addSpanOperationAttributes = addSpanAttributes("gen_ai.operation", String.camelToSnake)<OperationAttributes>
380const addSpanRequestAttributes = addSpanAttributes("gen_ai.request", String.camelToSnake)<RequestAttributes>
381const addSpanResponseAttributes = addSpanAttributes("gen_ai.response", String.camelToSnake)<ResponseAttributes>
382const addSpanTokenAttributes = addSpanAttributes("gen_ai.token", String.camelToSnake)<TokenAttributes>
383const addSpanUsageAttributes = addSpanAttributes("gen_ai.usage", String.camelToSnake)<UsageAttributes>
384
385/**
386 * Configuration options for GenAI telemetry attributes.
387 *
388 * **Details**
389 *
390 * Combines base attributes with optional grouped attributes for comprehensive
391 * telemetry coverage of AI operations.
392 *
393 * **Example** (Configuring GenAI telemetry attributes)
394 *
395 * ```ts import.meta.vitest
396 * import type { Telemetry } from "effect/ai"
397 *
398 * const telemetryOptions: Telemetry.GenAITelemetryAttributeOptions = {
399 * system: "openai",
400 * operation: {
401 * name: "chat"
402 * },
403 * request: {
404 * model: "gpt-4-turbo",
405 * temperature: 0.7,
406 * maxTokens: 2000
407 * },
408 * response: {
409 * id: "chatcmpl-123",
410 * model: "gpt-4-turbo-2024-04-09",
411 * finishReasons: ["stop"]
412 * },
413 * usage: {
414 * inputTokens: 50,
415 * outputTokens: 25
416 * }
417 * }
418 *
419 * const result = [telemetryOptions.system, telemetryOptions.usage?.inputTokens] // => ["openai", 50]
420 * ```
421 *
422 * @stability unstable
423 * @category options
424 * @since 4.0.0
425 */
426export type GenAITelemetryAttributeOptions = BaseAttributes & {
427 /**
428 * Operation-specific attributes (e.g., operation name).
429 */
430 readonly operation?: OperationAttributes | undefined
431 /**
432 * Request-specific attributes (e.g., model parameters).
433 */
434 readonly request?: RequestAttributes | undefined
435 /**
436 * Response-specific attributes (e.g., response metadata).
437 */
438 readonly response?: ResponseAttributes | undefined
439 /**
440 * Token-specific attributes.
441 */
442 readonly token?: TokenAttributes | undefined
443 /**
444 * Usage statistics attributes (e.g., token counts).
445 */
446 readonly usage?: UsageAttributes | undefined
447}
448
449/**
450 * Applies GenAI telemetry attributes to an OpenTelemetry span.
451 *
452 * **When to use**
453 *
454 * Use when you need to write GenAI request, response, token, or usage
455 * attributes onto an existing OpenTelemetry span.
456 *
457 * **Details**
458 *
459 * This function adds standardized GenAI attributes to a span following
460 * OpenTelemetry semantic conventions.
461 *
462 * **Gotchas**
463 *
464 * This function mutates the provided span in-place.
465 *
466 * **Example** (Adding GenAI telemetry annotations)
467 *
468 * ```ts import.meta.vitest
469 * import { Effect } from "effect"
470 * import { Telemetry } from "effect/ai"
471 *
472 * const directUsage = Effect.gen(function*() {
473 * const span = yield* Effect.currentSpan
474 *
475 * Telemetry.addGenAIAnnotations(span, {
476 * system: "openai",
477 * request: { model: "gpt-4", temperature: 0.7 },
478 * usage: { inputTokens: 100, outputTokens: 50 }
479 * })
480 * return (span as { attributes: ReadonlyMap<string, unknown> }).attributes.size
481 * })
482 *
483 * await Effect.runPromise(Effect.withSpan(directUsage, "example")) // => 5
484 * ```
485 *
486 * @stability unstable
487 * @category annotations
488 * @since 4.0.0
489 */
490export const addGenAIAnnotations: {
491 (options: GenAITelemetryAttributeOptions): (span: Span) => void
492 (span: Span, options: GenAITelemetryAttributeOptions): void
493} = dual(2, (span: Span, options: GenAITelemetryAttributeOptions) => {
494 addSpanBaseAttributes(span, { system: options.system })
495 if (Predicate.isNotNullish(options.operation)) addSpanOperationAttributes(span, options.operation)
496 if (Predicate.isNotNullish(options.request)) addSpanRequestAttributes(span, options.request)
497 if (Predicate.isNotNullish(options.response)) addSpanResponseAttributes(span, options.response)
498 if (Predicate.isNotNullish(options.token)) addSpanTokenAttributes(span, options.token)
499 if (Predicate.isNotNullish(options.usage)) addSpanUsageAttributes(span, options.usage)
500})
501
502/**
503 * A function that can transform OpenTelemetry spans based on AI operation data.
504 *
505 * **Details**
506 *
507 * Span transformers receive the complete request/response context from AI operations
508 * and can add custom telemetry attributes, metrics, or other observability data.
509 *
510 * **Example** (Transforming AI spans)
511 *
512 * ```ts import.meta.vitest
513 * import type { Telemetry } from "effect/ai"
514 *
515 * const customTransformer: Telemetry.SpanTransformer = ({ response, span }) => {
516 * // Add custom attributes based on the response
517 * const textParts = response.filter((part) => part.type === "text")
518 * const totalTextLength = textParts.reduce(
519 * (sum, part) => sum + (part.type === "text" ? part.text.length : 0),
520 * 0
521 * )
522 * span.attribute("total_text_length", totalTextLength)
523 * }
524 *
525 * typeof customTransformer // => "function"
526 * ```
527 *
528 * @stability unstable
529 * @category models
530 * @since 4.0.0
531 */
532export interface SpanTransformer {
533 (
534 options: ProviderOptions & {
535 /**
536 * Array of response parts generated by the AI model.
537 */
538 readonly response: ReadonlyArray<Response.AllParts<any>>
539 }
540 ): void
541}
542
543/**
544 * Service tag for providing a `SpanTransformer` to large language model
545 * operations.
546 *
547 * **When to use**
548 *
549 * Use to retrieve or provide the current `SpanTransformer` through context for
550 * language model span annotation.
551 *
552 * @see {@link SpanTransformer} for the transformer contract provided by this service
553 *
554 * @stability unstable
555 * @category services
556 * @since 4.0.0
557 */
558export class CurrentSpanTransformer extends Context.Service<CurrentSpanTransformer, SpanTransformer>()(
559 "effect/ai/Telemetry/CurrentSpanTransformer"
560) {}