autotel-genai 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,8 @@ export const chat = traceGenAI({
68
68
  | **Trace** | `autotel-genai/trace` | `traceGenAI()`, `recordGenAiResponse/Usage` |
69
69
  | **Guard** | `autotel-genai/guard` | inline cost/token/loop kill-switch — `createGenAiBudget`, `createGenAiGuard`, `parseGuardRules` |
70
70
  | **Streaming** | `autotel-genai/streaming` | TTFC, throughput, inter-chunk distribution — `createStreamTimer`, `recordStreamTiming` |
71
- | **AI SDK** | `autotel-genai/ai-sdk` | Vercel `ai.*` `gen_ai.*` mapping + cost |
71
+ | **AI SDK** | `autotel-genai/observer` | `autotelTelemetry()` `registerTelemetry()` integration: live `gen_ai.*` spans + cost + streaming + nested traces + opt-in content. `subscribeAiTelemetry()` — zero-config `ai:telemetry` channel path |
72
+ | **AI SDK (legacy)** | `autotel-genai/ai-sdk` | `ai.*` → `gen_ai.*` mapping + cost for `LegacyOpenTelemetry`/older versions; `autotelEnrich()` for `@ai-sdk/otel` `enrichSpan` |
72
73
  | **Agents** | `autotel-genai/agent` | identity, delegation, policy, audit, privacy, non-repudiation |
73
74
 
74
75
  ### Cost
@@ -201,9 +202,75 @@ It honours spec breaking change #242 (`gen_ai.agent.id` is dropped on internal
201
202
 
202
203
  ### Vercel AI SDK
203
204
 
204
- The current AI SDK (`@ai-sdk/otel`'s `OpenTelemetry` integration) already emits
205
- `gen_ai.*` nothing to map. For `LegacyOpenTelemetry`/older versions, or to add
206
- cost:
205
+ Register `autotelTelemetry()` once and every `generateText` / `streamText` /
206
+ `embed` call streams a canonical `gen_ai.*` span tree — live, as it runs:
207
+
208
+ ```ts
209
+ import { registerTelemetry } from 'ai';
210
+ import { autotelTelemetry } from 'autotel-genai/observer';
211
+
212
+ registerTelemetry(autotelTelemetry()); // once, at startup
213
+ ```
214
+
215
+ See `apps/example-ai-sdk-observer` for a runnable AI SDK + Ollama demo
216
+ (generateText, tool loop, streamText timing, embeddings).
217
+
218
+ It implements the AI SDK's stable `Telemetry` lifecycle interface (ai v7+), so
219
+ it slots in exactly where `@ai-sdk/otel`'s `OpenTelemetry` does — but it also,
220
+ on every `chat` span:
221
+
222
+ - **prices the call** (`gen_ai.usage.cost.usd`) from `MODEL_PRICING`;
223
+ - records **streaming throughput** (`time_to_first_chunk`, `time_to_finish`,
224
+ `output_tokens_per_second`);
225
+ - keeps token usage on leaf `chat` spans only, so the `invoke_agent` root never
226
+ double-counts.
227
+
228
+ It is push-based and concurrency-safe (every event carries the SDK `callId`),
229
+ and it pulls in **no** dependency on `ai` — the returned object satisfies the
230
+ `Telemetry` interface structurally, so the snippet above type-checks as-is.
231
+ `rerank` has no canonical `gen_ai` operation and is intentionally not mapped.
232
+
233
+ **Nested traces.** It implements the SDK's `executeTool` / `executeLanguageModelCall`
234
+ context runners, so a tool whose `execute` calls `generateText` — and the
235
+ provider's own auto-instrumented HTTP spans — nest under the right span
236
+ automatically.
237
+
238
+ **Content capture (opt-in).** Off by default for privacy. Turn it on to record
239
+ prompts, responses, system instructions, and tool I/O, mapped to the
240
+ [GenAI SemConv message format](#genai-message-format). The SDK's per-call
241
+ `recordInputs` / `recordOutputs` are honored, and `exportContent` lets you redact
242
+ or drop content per event:
243
+
244
+ ```ts
245
+ registerTelemetry(
246
+ autotelTelemetry({
247
+ captureContent: true,
248
+ exportContent: (event) => redact(event), // optional: redact before write
249
+ }),
250
+ );
251
+ ```
252
+
253
+ **Zero-config (no `registerTelemetry`).** Subscribe to the SDK's `ai:telemetry`
254
+ Node tracing channel instead. The SDK publishes operation spans as soon as the
255
+ channel has a subscriber:
256
+
257
+ ```ts
258
+ import { subscribeAiTelemetry } from 'autotel-genai/observer';
259
+
260
+ const unsubscribe = subscribeAiTelemetry(); // once, at startup
261
+ ```
262
+
263
+ The channel path gives you the same `invoke_agent › chat › execute_tool` tree
264
+ with usage and cost, but not the per-call streaming timing (which only the
265
+ lifecycle `onLanguageModelCallEnd` event carries) — prefer
266
+ `registerTelemetry(autotelTelemetry())` when you can.
267
+
268
+ Register globally, or pass per-call via `telemetry.integrations` to scope it to
269
+ one call. For the **legacy** `LegacyOpenTelemetry`/older-version path, or to
270
+ enrich spans another integration already emitted, the attribute bridge maps
271
+ `ai.*` → `gen_ai.*` and adds cost; for versions before the `Telemetry` interface,
272
+ walk the finished result with `observeAiSdkResult` (see
273
+ [Observer](#observer-event-stream--spans)):
207
274
 
208
275
  ```ts
209
276
  import { mapAiSdkAttributes, recordAiSdkCost } from 'autotel-genai/ai-sdk';
@@ -212,6 +279,40 @@ const canonical = mapAiSdkAttributes(span.attributes); // ai.* → gen_ai.*
212
279
  recordAiSdkCost(ctx, span.attributes); // sets gen_ai.usage.cost.usd
213
280
  ```
214
281
 
282
+ #### Already using `@ai-sdk/otel`?
283
+
284
+ Drop `autotelEnrich()` into its `enrichSpan` to stamp autotel provenance and
285
+ promote your `runtimeContext` onto every span:
286
+
287
+ ```ts
288
+ import { OpenTelemetry } from '@ai-sdk/otel';
289
+ import { autotelEnrich } from 'autotel-genai/ai-sdk';
290
+
291
+ registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));
292
+ ```
293
+
294
+ `enrichSpan` **cannot add cost** — the SDK passes it only
295
+ `{ spanType, operationId, callId, runtimeContext }` (no usage, no model), and its
296
+ own attributes win over custom keys. To get `gen_ai.usage.cost.usd` on the model
297
+ span, use `autotelTelemetry()` (it owns span creation). Either way,
298
+ [`autotel-devtools`](../autotel-devtools) prices `gen_ai` spans on render, so cost
299
+ shows there regardless of which integration emitted them.
300
+
301
+ #### Local devtools, one line
302
+
303
+ Point an OTLP exporter at a running `autotel-devtools` receiver and you get a
304
+ live GenAI run view — cost, token breakdown, tool timeline, and a narrated
305
+ "Explain run" walkthrough — that works in production too (unlike
306
+ `@ai-sdk/devtools`, which is dev-only):
307
+
308
+ ```ts
309
+ import { registerTelemetry } from 'ai';
310
+ import { autotelTelemetry } from 'autotel-genai/observer';
311
+
312
+ registerTelemetry(autotelTelemetry()); // → your OTLP pipeline → autotel-devtools
313
+ // npx autotel-devtools → http://localhost:4318
314
+ ```
315
+
215
316
  ### Observer (event-stream → spans)
216
317
 
217
318
  When you instrument a framework that emits its own lifecycle stream (agent
@@ -122,9 +122,40 @@ function recordAiSdkCost(ctx, attributes, options) {
122
122
  if (cost !== void 0) ctx.setAttribute(require_semconv.GEN_AI.USAGE_COST_USD, cost);
123
123
  return cost;
124
124
  }
125
+ /** Marks a span as having passed through an autotel-aware `enrichSpan`. */
126
+ const AUTOTEL_ENRICHED_ATTR = "autotel.enriched";
127
+ /**
128
+ * Build an `enrichSpan` callback for the `@ai-sdk/otel` `OpenTelemetry`
129
+ * integration. It stamps an autotel provenance marker and merges any attributes
130
+ * your `attributes` mapper returns:
131
+ *
132
+ * ```ts
133
+ * import { registerTelemetry } from 'ai';
134
+ * import { OpenTelemetry } from '@ai-sdk/otel';
135
+ * import { autotelEnrich } from 'autotel-genai/ai-sdk';
136
+ *
137
+ * registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));
138
+ * ```
139
+ *
140
+ * Important: `enrichSpan` **cannot add cost**. The AI SDK only passes
141
+ * `{ spanType, operationId, callId, runtimeContext }` to the callback — no token
142
+ * usage and no resolved model — and its own attributes override custom keys. For
143
+ * `gen_ai.usage.cost.usd` on the model span, use `autotelTelemetry()` from
144
+ * `autotel-genai/observer` (it owns span creation), or price spans after the
145
+ * fact with {@link estimateAiSdkCost}. `autotel-devtools` also prices `gen_ai`
146
+ * spans on render regardless of which integration emitted them.
147
+ */
148
+ function autotelEnrich(options = {}) {
149
+ return (ctx) => ({
150
+ [AUTOTEL_ENRICHED_ATTR]: true,
151
+ ...options.attributes?.(ctx)
152
+ });
153
+ }
125
154
 
126
155
  //#endregion
127
156
  exports.AI_SDK_ATTR = AI_SDK_ATTR;
157
+ exports.AUTOTEL_ENRICHED_ATTR = AUTOTEL_ENRICHED_ATTR;
158
+ exports.autotelEnrich = autotelEnrich;
128
159
  exports.estimateAiSdkCost = estimateAiSdkCost;
129
160
  exports.extractAiSdkModel = extractAiSdkModel;
130
161
  exports.extractAiSdkUsage = extractAiSdkUsage;
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-bridge.cjs","names":["GEN_AI_PROVIDER","GEN_AI","genAiUsageAttributes","estimateLLMCost"],"sources":["../src/ai-sdk-bridge.ts"],"sourcesContent":["/**\n * Vercel AI SDK interop.\n *\n * The current Vercel AI SDK (`@ai-sdk/otel`'s `OpenTelemetry` integration,\n * stable since v7) already emits canonical `gen_ai.*` attributes and the\n * `invoke_agent {model}` › `chat {model}` › `execute_tool {tool}` span\n * hierarchy — so for new code there is nothing to map.\n *\n * This module exists for the two cases that still need help:\n *\n * 1. **Legacy `ai.*` attributes** — spans from `LegacyOpenTelemetry` or older\n * AI SDK versions. {@link mapAiSdkAttributes} rewrites them to `gen_ai.*`.\n * 2. **Cost enrichment** — neither integration emits cost. Pull usage from a\n * span's attributes (canonical *or* legacy) with {@link extractAiSdkUsage}\n * and price it, or copy a canonical `gen_ai.usage.cost.usd` onto your own\n * wrapping span with {@link recordAiSdkCost}.\n *\n * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry\n */\n\nimport type { TraceContext } from 'autotel';\nimport {\n genAiUsageAttributes,\n type GenAiAttributeMap,\n} from './attributes.js';\nimport {\n estimateLLMCost,\n type EstimateCostOptions,\n type TokenUsage,\n} from './cost.js';\nimport {\n GEN_AI,\n GEN_AI_PROVIDER,\n type GenAiProviderName,\n} from './semconv.js';\n\n/** Legacy AI SDK (`LegacyOpenTelemetry`) attribute keys we understand. */\nexport const AI_SDK_ATTR = {\n MODEL_ID: 'ai.model.id',\n MODEL_PROVIDER: 'ai.model.provider',\n RESPONSE_MODEL: 'ai.response.model',\n RESPONSE_ID: 'ai.response.id',\n RESPONSE_FINISH_REASON: 'ai.response.finishReason',\n USAGE_PROMPT_TOKENS: 'ai.usage.promptTokens',\n USAGE_INPUT_TOKENS: 'ai.usage.inputTokens',\n USAGE_COMPLETION_TOKENS: 'ai.usage.completionTokens',\n USAGE_OUTPUT_TOKENS: 'ai.usage.outputTokens',\n USAGE_CACHED_INPUT_TOKENS: 'ai.usage.cachedInputTokens',\n USAGE_REASONING_TOKENS: 'ai.usage.reasoningTokens',\n SETTINGS_MAX_TOKENS: 'ai.settings.maxOutputTokens',\n TELEMETRY_FUNCTION_ID: 'ai.telemetry.functionId',\n} as const;\n\nconst PROVIDER_PREFIX_MAP: Record<string, GenAiProviderName> = {\n openai: GEN_AI_PROVIDER.OPENAI,\n azure: GEN_AI_PROVIDER.AZURE_AI_OPENAI,\n anthropic: GEN_AI_PROVIDER.ANTHROPIC,\n google: GEN_AI_PROVIDER.GCP_GEMINI,\n 'google-vertex': GEN_AI_PROVIDER.GCP_VERTEX_AI,\n vertex: GEN_AI_PROVIDER.GCP_VERTEX_AI,\n 'amazon-bedrock': GEN_AI_PROVIDER.AWS_BEDROCK,\n bedrock: GEN_AI_PROVIDER.AWS_BEDROCK,\n cohere: GEN_AI_PROVIDER.COHERE,\n mistral: GEN_AI_PROVIDER.MISTRAL_AI,\n groq: GEN_AI_PROVIDER.GROQ,\n deepseek: GEN_AI_PROVIDER.DEEPSEEK,\n perplexity: GEN_AI_PROVIDER.PERPLEXITY,\n xai: GEN_AI_PROVIDER.X_AI,\n};\n\n/**\n * Normalize an AI SDK provider id (e.g. `openai.chat`, `amazon-bedrock`,\n * `google.generative-ai`) to a canonical `gen_ai.provider.name` value. Returns\n * the original string when it isn't a known provider.\n */\nexport function normalizeAiSdkProvider(provider: string): GenAiProviderName {\n const head = provider.split('.')[0]?.toLowerCase() ?? provider;\n return PROVIDER_PREFIX_MAP[head] ?? provider;\n}\n\nfunction num(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * Extract {@link TokenUsage} from a span's attributes, reading canonical\n * `gen_ai.usage.*` first and falling back to legacy `ai.usage.*`. Returns\n * `undefined` when no token counts are present.\n */\nexport function extractAiSdkUsage(\n attributes: Record<string, unknown>,\n): TokenUsage | undefined {\n const inputTokens =\n num(attributes[GEN_AI.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_PROMPT_TOKENS]);\n const outputTokens =\n num(attributes[GEN_AI.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_COMPLETION_TOKENS]);\n const cacheReadInputTokens =\n num(attributes[GEN_AI.USAGE_CACHE_READ_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_CACHED_INPUT_TOKENS]);\n const reasoningOutputTokens =\n num(attributes[GEN_AI.USAGE_REASONING_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_REASONING_TOKENS]);\n const cacheCreationInputTokens = num(\n attributes[GEN_AI.USAGE_CACHE_CREATION_INPUT_TOKENS],\n );\n\n if (\n inputTokens === undefined &&\n outputTokens === undefined &&\n cacheReadInputTokens === undefined &&\n reasoningOutputTokens === undefined &&\n cacheCreationInputTokens === undefined\n ) {\n return undefined;\n }\n return {\n inputTokens,\n outputTokens,\n reasoningOutputTokens,\n cacheReadInputTokens,\n cacheCreationInputTokens,\n };\n}\n\n/** Read the request model from canonical or legacy attributes. */\nexport function extractAiSdkModel(\n attributes: Record<string, unknown>,\n): string | undefined {\n return (\n str(attributes[GEN_AI.REQUEST_MODEL]) ?? str(attributes[AI_SDK_ATTR.MODEL_ID])\n );\n}\n\n/**\n * Rewrite legacy `ai.*` telemetry attributes to canonical `gen_ai.*`. Pass the\n * attributes of an AI SDK span emitted by `LegacyOpenTelemetry` (or an older\n * SDK version); returns a fresh map with the canonical keys. Unknown keys are\n * dropped — this is a focused mapper, not a passthrough.\n */\nexport function mapAiSdkAttributes(\n attributes: Record<string, unknown>,\n): GenAiAttributeMap {\n const out: GenAiAttributeMap = {};\n\n const model = str(attributes[AI_SDK_ATTR.MODEL_ID]);\n if (model) out[GEN_AI.REQUEST_MODEL] = model;\n\n const provider = str(attributes[AI_SDK_ATTR.MODEL_PROVIDER]);\n if (provider) out[GEN_AI.PROVIDER_NAME] = normalizeAiSdkProvider(provider);\n\n const responseModel = str(attributes[AI_SDK_ATTR.RESPONSE_MODEL]);\n if (responseModel) out[GEN_AI.RESPONSE_MODEL] = responseModel;\n\n const responseId = str(attributes[AI_SDK_ATTR.RESPONSE_ID]);\n if (responseId) out[GEN_AI.RESPONSE_ID] = responseId;\n\n const finishReason = str(attributes[AI_SDK_ATTR.RESPONSE_FINISH_REASON]);\n if (finishReason) out[GEN_AI.RESPONSE_FINISH_REASONS] = [finishReason];\n\n const maxTokens = num(attributes[AI_SDK_ATTR.SETTINGS_MAX_TOKENS]);\n if (maxTokens !== undefined) out[GEN_AI.REQUEST_MAX_TOKENS] = maxTokens;\n\n const functionId = str(attributes[AI_SDK_ATTR.TELEMETRY_FUNCTION_ID]);\n if (functionId) out[GEN_AI.AGENT_NAME] = functionId;\n\n const usage = extractAiSdkUsage(attributes);\n if (usage) Object.assign(out, genAiUsageAttributes(usage));\n\n return out;\n}\n\n/**\n * Estimate the USD cost of an AI SDK call from a span's attributes (model +\n * usage, canonical or legacy). Returns `undefined` when model or usage is\n * missing, or the model has no known pricing.\n */\nexport function estimateAiSdkCost(\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const model = extractAiSdkModel(attributes);\n const usage = extractAiSdkUsage(attributes);\n if (!model || !usage) return undefined;\n return estimateLLMCost(model, usage, options);\n}\n\n/**\n * Estimate cost from AI SDK span attributes and record it as\n * `gen_ai.usage.cost.usd` on your own wrapping trace context. Useful when you\n * wrap a `generateText`/`streamText` call in an autotel span and want cost on\n * the parent. Returns the estimated cost, or `undefined`.\n */\nexport function recordAiSdkCost(\n ctx: Pick<TraceContext, 'setAttribute'>,\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const cost = estimateAiSdkCost(attributes, options);\n if (cost !== undefined) ctx.setAttribute(GEN_AI.USAGE_COST_USD, cost);\n return cost;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai-sdk-bridge.cjs","names":["GEN_AI_PROVIDER","GEN_AI","genAiUsageAttributes","estimateLLMCost"],"sources":["../src/ai-sdk-bridge.ts"],"sourcesContent":["/**\n * Vercel AI SDK interop.\n *\n * The current Vercel AI SDK (`@ai-sdk/otel`'s `OpenTelemetry` integration,\n * stable since v7) already emits canonical `gen_ai.*` attributes and the\n * `invoke_agent {model}` › `chat {model}` › `execute_tool {tool}` span\n * hierarchy — so for new code there is nothing to map.\n *\n * This module exists for the two cases that still need help:\n *\n * 1. **Legacy `ai.*` attributes** — spans from `LegacyOpenTelemetry` or older\n * AI SDK versions. {@link mapAiSdkAttributes} rewrites them to `gen_ai.*`.\n * 2. **Cost enrichment** — neither integration emits cost. Pull usage from a\n * span's attributes (canonical *or* legacy) with {@link extractAiSdkUsage}\n * and price it, or copy a canonical `gen_ai.usage.cost.usd` onto your own\n * wrapping span with {@link recordAiSdkCost}.\n *\n * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry\n */\n\nimport type { TraceContext } from 'autotel';\nimport {\n genAiUsageAttributes,\n type GenAiAttributeMap,\n} from './attributes.js';\nimport {\n estimateLLMCost,\n type EstimateCostOptions,\n type TokenUsage,\n} from './cost.js';\nimport {\n GEN_AI,\n GEN_AI_PROVIDER,\n type GenAiProviderName,\n} from './semconv.js';\n\n/** Legacy AI SDK (`LegacyOpenTelemetry`) attribute keys we understand. */\nexport const AI_SDK_ATTR = {\n MODEL_ID: 'ai.model.id',\n MODEL_PROVIDER: 'ai.model.provider',\n RESPONSE_MODEL: 'ai.response.model',\n RESPONSE_ID: 'ai.response.id',\n RESPONSE_FINISH_REASON: 'ai.response.finishReason',\n USAGE_PROMPT_TOKENS: 'ai.usage.promptTokens',\n USAGE_INPUT_TOKENS: 'ai.usage.inputTokens',\n USAGE_COMPLETION_TOKENS: 'ai.usage.completionTokens',\n USAGE_OUTPUT_TOKENS: 'ai.usage.outputTokens',\n USAGE_CACHED_INPUT_TOKENS: 'ai.usage.cachedInputTokens',\n USAGE_REASONING_TOKENS: 'ai.usage.reasoningTokens',\n SETTINGS_MAX_TOKENS: 'ai.settings.maxOutputTokens',\n TELEMETRY_FUNCTION_ID: 'ai.telemetry.functionId',\n} as const;\n\nconst PROVIDER_PREFIX_MAP: Record<string, GenAiProviderName> = {\n openai: GEN_AI_PROVIDER.OPENAI,\n azure: GEN_AI_PROVIDER.AZURE_AI_OPENAI,\n anthropic: GEN_AI_PROVIDER.ANTHROPIC,\n google: GEN_AI_PROVIDER.GCP_GEMINI,\n 'google-vertex': GEN_AI_PROVIDER.GCP_VERTEX_AI,\n vertex: GEN_AI_PROVIDER.GCP_VERTEX_AI,\n 'amazon-bedrock': GEN_AI_PROVIDER.AWS_BEDROCK,\n bedrock: GEN_AI_PROVIDER.AWS_BEDROCK,\n cohere: GEN_AI_PROVIDER.COHERE,\n mistral: GEN_AI_PROVIDER.MISTRAL_AI,\n groq: GEN_AI_PROVIDER.GROQ,\n deepseek: GEN_AI_PROVIDER.DEEPSEEK,\n perplexity: GEN_AI_PROVIDER.PERPLEXITY,\n xai: GEN_AI_PROVIDER.X_AI,\n};\n\n/**\n * Normalize an AI SDK provider id (e.g. `openai.chat`, `amazon-bedrock`,\n * `google.generative-ai`) to a canonical `gen_ai.provider.name` value. Returns\n * the original string when it isn't a known provider.\n */\nexport function normalizeAiSdkProvider(provider: string): GenAiProviderName {\n const head = provider.split('.')[0]?.toLowerCase() ?? provider;\n return PROVIDER_PREFIX_MAP[head] ?? provider;\n}\n\nfunction num(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * Extract {@link TokenUsage} from a span's attributes, reading canonical\n * `gen_ai.usage.*` first and falling back to legacy `ai.usage.*`. Returns\n * `undefined` when no token counts are present.\n */\nexport function extractAiSdkUsage(\n attributes: Record<string, unknown>,\n): TokenUsage | undefined {\n const inputTokens =\n num(attributes[GEN_AI.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_PROMPT_TOKENS]);\n const outputTokens =\n num(attributes[GEN_AI.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_COMPLETION_TOKENS]);\n const cacheReadInputTokens =\n num(attributes[GEN_AI.USAGE_CACHE_READ_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_CACHED_INPUT_TOKENS]);\n const reasoningOutputTokens =\n num(attributes[GEN_AI.USAGE_REASONING_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_REASONING_TOKENS]);\n const cacheCreationInputTokens = num(\n attributes[GEN_AI.USAGE_CACHE_CREATION_INPUT_TOKENS],\n );\n\n if (\n inputTokens === undefined &&\n outputTokens === undefined &&\n cacheReadInputTokens === undefined &&\n reasoningOutputTokens === undefined &&\n cacheCreationInputTokens === undefined\n ) {\n return undefined;\n }\n return {\n inputTokens,\n outputTokens,\n reasoningOutputTokens,\n cacheReadInputTokens,\n cacheCreationInputTokens,\n };\n}\n\n/** Read the request model from canonical or legacy attributes. */\nexport function extractAiSdkModel(\n attributes: Record<string, unknown>,\n): string | undefined {\n return (\n str(attributes[GEN_AI.REQUEST_MODEL]) ?? str(attributes[AI_SDK_ATTR.MODEL_ID])\n );\n}\n\n/**\n * Rewrite legacy `ai.*` telemetry attributes to canonical `gen_ai.*`. Pass the\n * attributes of an AI SDK span emitted by `LegacyOpenTelemetry` (or an older\n * SDK version); returns a fresh map with the canonical keys. Unknown keys are\n * dropped — this is a focused mapper, not a passthrough.\n */\nexport function mapAiSdkAttributes(\n attributes: Record<string, unknown>,\n): GenAiAttributeMap {\n const out: GenAiAttributeMap = {};\n\n const model = str(attributes[AI_SDK_ATTR.MODEL_ID]);\n if (model) out[GEN_AI.REQUEST_MODEL] = model;\n\n const provider = str(attributes[AI_SDK_ATTR.MODEL_PROVIDER]);\n if (provider) out[GEN_AI.PROVIDER_NAME] = normalizeAiSdkProvider(provider);\n\n const responseModel = str(attributes[AI_SDK_ATTR.RESPONSE_MODEL]);\n if (responseModel) out[GEN_AI.RESPONSE_MODEL] = responseModel;\n\n const responseId = str(attributes[AI_SDK_ATTR.RESPONSE_ID]);\n if (responseId) out[GEN_AI.RESPONSE_ID] = responseId;\n\n const finishReason = str(attributes[AI_SDK_ATTR.RESPONSE_FINISH_REASON]);\n if (finishReason) out[GEN_AI.RESPONSE_FINISH_REASONS] = [finishReason];\n\n const maxTokens = num(attributes[AI_SDK_ATTR.SETTINGS_MAX_TOKENS]);\n if (maxTokens !== undefined) out[GEN_AI.REQUEST_MAX_TOKENS] = maxTokens;\n\n const functionId = str(attributes[AI_SDK_ATTR.TELEMETRY_FUNCTION_ID]);\n if (functionId) out[GEN_AI.AGENT_NAME] = functionId;\n\n const usage = extractAiSdkUsage(attributes);\n if (usage) Object.assign(out, genAiUsageAttributes(usage));\n\n return out;\n}\n\n/**\n * Estimate the USD cost of an AI SDK call from a span's attributes (model +\n * usage, canonical or legacy). Returns `undefined` when model or usage is\n * missing, or the model has no known pricing.\n */\nexport function estimateAiSdkCost(\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const model = extractAiSdkModel(attributes);\n const usage = extractAiSdkUsage(attributes);\n if (!model || !usage) return undefined;\n return estimateLLMCost(model, usage, options);\n}\n\n/**\n * Estimate cost from AI SDK span attributes and record it as\n * `gen_ai.usage.cost.usd` on your own wrapping trace context. Useful when you\n * wrap a `generateText`/`streamText` call in an autotel span and want cost on\n * the parent. Returns the estimated cost, or `undefined`.\n */\nexport function recordAiSdkCost(\n ctx: Pick<TraceContext, 'setAttribute'>,\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const cost = estimateAiSdkCost(attributes, options);\n if (cost !== undefined) ctx.setAttribute(GEN_AI.USAGE_COST_USD, cost);\n return cost;\n}\n\n// --- `@ai-sdk/otel` enrichSpan interop -------------------------------------\n\n/** Marks a span as having passed through an autotel-aware `enrichSpan`. */\nexport const AUTOTEL_ENRICHED_ATTR = 'autotel.enriched';\n\n/** Span kinds the `@ai-sdk/otel` `OpenTelemetry` integration emits. */\nexport type AiSdkSpanType =\n | 'operation'\n | 'step'\n | 'languageModel'\n | 'tool'\n | 'embedding'\n | 'reranking';\n\n/** The context `@ai-sdk/otel` passes to an `enrichSpan` callback. */\nexport interface AiSdkEnrichContext {\n spanType: AiSdkSpanType;\n operationId: string;\n callId: string;\n runtimeContext?: Record<string, unknown>;\n}\n\nexport interface AutotelEnrichOptions {\n /**\n * Map the enrich context to extra span attributes — e.g. promote\n * `runtimeContext` fields (sessionId, tenantId) onto the span. Returns\n * `undefined` to add nothing for that span.\n */\n attributes?: (\n ctx: AiSdkEnrichContext,\n ) => Record<string, string | number | boolean> | undefined;\n}\n\n/**\n * Build an `enrichSpan` callback for the `@ai-sdk/otel` `OpenTelemetry`\n * integration. It stamps an autotel provenance marker and merges any attributes\n * your `attributes` mapper returns:\n *\n * ```ts\n * import { registerTelemetry } from 'ai';\n * import { OpenTelemetry } from '@ai-sdk/otel';\n * import { autotelEnrich } from 'autotel-genai/ai-sdk';\n *\n * registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));\n * ```\n *\n * Important: `enrichSpan` **cannot add cost**. The AI SDK only passes\n * `{ spanType, operationId, callId, runtimeContext }` to the callback — no token\n * usage and no resolved model — and its own attributes override custom keys. For\n * `gen_ai.usage.cost.usd` on the model span, use `autotelTelemetry()` from\n * `autotel-genai/observer` (it owns span creation), or price spans after the\n * fact with {@link estimateAiSdkCost}. `autotel-devtools` also prices `gen_ai`\n * spans on render regardless of which integration emitted them.\n */\nexport function autotelEnrich(\n options: AutotelEnrichOptions = {},\n): (ctx: AiSdkEnrichContext) => Record<string, string | number | boolean> {\n return (ctx) => ({\n [AUTOTEL_ENRICHED_ATTR]: true,\n ...options.attributes?.(ctx),\n });\n}\n"],"mappings":"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"}
@@ -54,6 +54,47 @@ declare function estimateAiSdkCost(attributes: Record<string, unknown>, options?
54
54
  * the parent. Returns the estimated cost, or `undefined`.
55
55
  */
56
56
  declare function recordAiSdkCost(ctx: Pick<TraceContext, 'setAttribute'>, attributes: Record<string, unknown>, options?: EstimateCostOptions): number | undefined;
57
+ /** Marks a span as having passed through an autotel-aware `enrichSpan`. */
58
+ declare const AUTOTEL_ENRICHED_ATTR = "autotel.enriched";
59
+ /** Span kinds the `@ai-sdk/otel` `OpenTelemetry` integration emits. */
60
+ type AiSdkSpanType = 'operation' | 'step' | 'languageModel' | 'tool' | 'embedding' | 'reranking';
61
+ /** The context `@ai-sdk/otel` passes to an `enrichSpan` callback. */
62
+ interface AiSdkEnrichContext {
63
+ spanType: AiSdkSpanType;
64
+ operationId: string;
65
+ callId: string;
66
+ runtimeContext?: Record<string, unknown>;
67
+ }
68
+ interface AutotelEnrichOptions {
69
+ /**
70
+ * Map the enrich context to extra span attributes — e.g. promote
71
+ * `runtimeContext` fields (sessionId, tenantId) onto the span. Returns
72
+ * `undefined` to add nothing for that span.
73
+ */
74
+ attributes?: (ctx: AiSdkEnrichContext) => Record<string, string | number | boolean> | undefined;
75
+ }
76
+ /**
77
+ * Build an `enrichSpan` callback for the `@ai-sdk/otel` `OpenTelemetry`
78
+ * integration. It stamps an autotel provenance marker and merges any attributes
79
+ * your `attributes` mapper returns:
80
+ *
81
+ * ```ts
82
+ * import { registerTelemetry } from 'ai';
83
+ * import { OpenTelemetry } from '@ai-sdk/otel';
84
+ * import { autotelEnrich } from 'autotel-genai/ai-sdk';
85
+ *
86
+ * registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));
87
+ * ```
88
+ *
89
+ * Important: `enrichSpan` **cannot add cost**. The AI SDK only passes
90
+ * `{ spanType, operationId, callId, runtimeContext }` to the callback — no token
91
+ * usage and no resolved model — and its own attributes override custom keys. For
92
+ * `gen_ai.usage.cost.usd` on the model span, use `autotelTelemetry()` from
93
+ * `autotel-genai/observer` (it owns span creation), or price spans after the
94
+ * fact with {@link estimateAiSdkCost}. `autotel-devtools` also prices `gen_ai`
95
+ * spans on render regardless of which integration emitted them.
96
+ */
97
+ declare function autotelEnrich(options?: AutotelEnrichOptions): (ctx: AiSdkEnrichContext) => Record<string, string | number | boolean>;
57
98
  //#endregion
58
- export { AI_SDK_ATTR, estimateAiSdkCost, extractAiSdkModel, extractAiSdkUsage, mapAiSdkAttributes, normalizeAiSdkProvider, recordAiSdkCost };
99
+ export { AI_SDK_ATTR, AUTOTEL_ENRICHED_ATTR, AiSdkEnrichContext, AiSdkSpanType, AutotelEnrichOptions, autotelEnrich, estimateAiSdkCost, extractAiSdkModel, extractAiSdkUsage, mapAiSdkAttributes, normalizeAiSdkProvider, recordAiSdkCost };
59
100
  //# sourceMappingURL=ai-sdk-bridge.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-bridge.d.cts","names":[],"sources":["../src/ai-sdk-bridge.ts"],"mappings":";;;;;;;cAqCa,WAAA;EAAA;;;;;;;;;;;;;;AAgGb;;;;AACqC;AADrC,iBA1DgB,sBAAA,CAAuB,QAAA,WAAmB,iBAAiB;;;;;;iBAkB3D,iBAAA,CACd,UAAA,EAAY,MAAA,oBACX,UAAU;;iBAsCG,iBAAA,CACd,UAAmC,EAAvB,MAAM;AAkDpB;;;;;;AAAA,iBArCgB,kBAAA,CACd,UAAA,EAAY,MAAA,oBACX,iBAAiB;;;AAqCW;AAc/B;;iBAhBgB,iBAAA,CACd,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAmB;;;;;;;iBAcf,eAAA,CACd,GAAA,EAAK,IAAA,CAAK,YAAA,mBACV,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAA"}
1
+ {"version":3,"file":"ai-sdk-bridge.d.cts","names":[],"sources":["../src/ai-sdk-bridge.ts"],"mappings":";;;;;;;cAqCa,WAAA;EAAA;;;;;;;;;;;;;;AAgGb;;;;AACqC;AADrC,iBA1DgB,sBAAA,CAAuB,QAAA,WAAmB,iBAAiB;;;;;;iBAkB3D,iBAAA,CACd,UAAA,EAAY,MAAA,oBACX,UAAU;;iBAsCG,iBAAA,CACd,UAAmC,EAAvB,MAAM;AAkDpB;;;;;;AAAA,iBArCgB,kBAAA,CACd,UAAA,EAAY,MAAA,oBACX,iBAAiB;;;AAqCW;AAc/B;;iBAhBgB,iBAAA,CACd,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAmB;;;;;;;iBAcf,eAAA,CACd,GAAA,EAAK,IAAA,CAAK,YAAA,mBACV,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAA;;cAUC,qBAAA;;KAGD,aAAA;;UASK,kBAAA;EACf,QAAA,EAAU,aAAA;EACV,WAAA;EACA,MAAA;EACA,cAAA,GAAiB,MAAM;AAAA;AAAA,UAGR,oBAAA;EAnBiB;AAGlC;;;;EAsBE,UAAA,IACE,GAAA,EAAK,kBAAA,KACF,MAAM;AAAA;;;;;;;;;;;AAXY;AAGzB;;;;;;;;;AAQa;iBAwBG,aAAA,CACd,OAAA,GAAS,oBAAA,IACP,GAAA,EAAK,kBAAA,KAAuB,MAAA"}
@@ -54,6 +54,47 @@ declare function estimateAiSdkCost(attributes: Record<string, unknown>, options?
54
54
  * the parent. Returns the estimated cost, or `undefined`.
55
55
  */
56
56
  declare function recordAiSdkCost(ctx: Pick<TraceContext, 'setAttribute'>, attributes: Record<string, unknown>, options?: EstimateCostOptions): number | undefined;
57
+ /** Marks a span as having passed through an autotel-aware `enrichSpan`. */
58
+ declare const AUTOTEL_ENRICHED_ATTR = "autotel.enriched";
59
+ /** Span kinds the `@ai-sdk/otel` `OpenTelemetry` integration emits. */
60
+ type AiSdkSpanType = 'operation' | 'step' | 'languageModel' | 'tool' | 'embedding' | 'reranking';
61
+ /** The context `@ai-sdk/otel` passes to an `enrichSpan` callback. */
62
+ interface AiSdkEnrichContext {
63
+ spanType: AiSdkSpanType;
64
+ operationId: string;
65
+ callId: string;
66
+ runtimeContext?: Record<string, unknown>;
67
+ }
68
+ interface AutotelEnrichOptions {
69
+ /**
70
+ * Map the enrich context to extra span attributes — e.g. promote
71
+ * `runtimeContext` fields (sessionId, tenantId) onto the span. Returns
72
+ * `undefined` to add nothing for that span.
73
+ */
74
+ attributes?: (ctx: AiSdkEnrichContext) => Record<string, string | number | boolean> | undefined;
75
+ }
76
+ /**
77
+ * Build an `enrichSpan` callback for the `@ai-sdk/otel` `OpenTelemetry`
78
+ * integration. It stamps an autotel provenance marker and merges any attributes
79
+ * your `attributes` mapper returns:
80
+ *
81
+ * ```ts
82
+ * import { registerTelemetry } from 'ai';
83
+ * import { OpenTelemetry } from '@ai-sdk/otel';
84
+ * import { autotelEnrich } from 'autotel-genai/ai-sdk';
85
+ *
86
+ * registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));
87
+ * ```
88
+ *
89
+ * Important: `enrichSpan` **cannot add cost**. The AI SDK only passes
90
+ * `{ spanType, operationId, callId, runtimeContext }` to the callback — no token
91
+ * usage and no resolved model — and its own attributes override custom keys. For
92
+ * `gen_ai.usage.cost.usd` on the model span, use `autotelTelemetry()` from
93
+ * `autotel-genai/observer` (it owns span creation), or price spans after the
94
+ * fact with {@link estimateAiSdkCost}. `autotel-devtools` also prices `gen_ai`
95
+ * spans on render regardless of which integration emitted them.
96
+ */
97
+ declare function autotelEnrich(options?: AutotelEnrichOptions): (ctx: AiSdkEnrichContext) => Record<string, string | number | boolean>;
57
98
  //#endregion
58
- export { AI_SDK_ATTR, estimateAiSdkCost, extractAiSdkModel, extractAiSdkUsage, mapAiSdkAttributes, normalizeAiSdkProvider, recordAiSdkCost };
99
+ export { AI_SDK_ATTR, AUTOTEL_ENRICHED_ATTR, AiSdkEnrichContext, AiSdkSpanType, AutotelEnrichOptions, autotelEnrich, estimateAiSdkCost, extractAiSdkModel, extractAiSdkUsage, mapAiSdkAttributes, normalizeAiSdkProvider, recordAiSdkCost };
59
100
  //# sourceMappingURL=ai-sdk-bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-bridge.d.ts","names":[],"sources":["../src/ai-sdk-bridge.ts"],"mappings":";;;;;;;cAqCa,WAAA;EAAA;;;;;;;;;;;;;;AAgGb;;;;AACqC;AADrC,iBA1DgB,sBAAA,CAAuB,QAAA,WAAmB,iBAAiB;;;;;;iBAkB3D,iBAAA,CACd,UAAA,EAAY,MAAA,oBACX,UAAU;;iBAsCG,iBAAA,CACd,UAAmC,EAAvB,MAAM;AAkDpB;;;;;;AAAA,iBArCgB,kBAAA,CACd,UAAA,EAAY,MAAA,oBACX,iBAAiB;;;AAqCW;AAc/B;;iBAhBgB,iBAAA,CACd,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAmB;;;;;;;iBAcf,eAAA,CACd,GAAA,EAAK,IAAA,CAAK,YAAA,mBACV,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAA"}
1
+ {"version":3,"file":"ai-sdk-bridge.d.ts","names":[],"sources":["../src/ai-sdk-bridge.ts"],"mappings":";;;;;;;cAqCa,WAAA;EAAA;;;;;;;;;;;;;;AAgGb;;;;AACqC;AADrC,iBA1DgB,sBAAA,CAAuB,QAAA,WAAmB,iBAAiB;;;;;;iBAkB3D,iBAAA,CACd,UAAA,EAAY,MAAA,oBACX,UAAU;;iBAsCG,iBAAA,CACd,UAAmC,EAAvB,MAAM;AAkDpB;;;;;;AAAA,iBArCgB,kBAAA,CACd,UAAA,EAAY,MAAA,oBACX,iBAAiB;;;AAqCW;AAc/B;;iBAhBgB,iBAAA,CACd,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAmB;;;;;;;iBAcf,eAAA,CACd,GAAA,EAAK,IAAA,CAAK,YAAA,mBACV,UAAA,EAAY,MAAA,mBACZ,OAAA,GAAU,mBAAA;;cAUC,qBAAA;;KAGD,aAAA;;UASK,kBAAA;EACf,QAAA,EAAU,aAAA;EACV,WAAA;EACA,MAAA;EACA,cAAA,GAAiB,MAAM;AAAA;AAAA,UAGR,oBAAA;EAnBiB;AAGlC;;;;EAsBE,UAAA,IACE,GAAA,EAAK,kBAAA,KACF,MAAM;AAAA;;;;;;;;;;;AAXY;AAGzB;;;;;;;;;AAQa;iBAwBG,aAAA,CACd,OAAA,GAAS,oBAAA,IACP,GAAA,EAAK,kBAAA,KAAuB,MAAA"}
@@ -121,7 +121,36 @@ function recordAiSdkCost(ctx, attributes, options) {
121
121
  if (cost !== void 0) ctx.setAttribute(GEN_AI.USAGE_COST_USD, cost);
122
122
  return cost;
123
123
  }
124
+ /** Marks a span as having passed through an autotel-aware `enrichSpan`. */
125
+ const AUTOTEL_ENRICHED_ATTR = "autotel.enriched";
126
+ /**
127
+ * Build an `enrichSpan` callback for the `@ai-sdk/otel` `OpenTelemetry`
128
+ * integration. It stamps an autotel provenance marker and merges any attributes
129
+ * your `attributes` mapper returns:
130
+ *
131
+ * ```ts
132
+ * import { registerTelemetry } from 'ai';
133
+ * import { OpenTelemetry } from '@ai-sdk/otel';
134
+ * import { autotelEnrich } from 'autotel-genai/ai-sdk';
135
+ *
136
+ * registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));
137
+ * ```
138
+ *
139
+ * Important: `enrichSpan` **cannot add cost**. The AI SDK only passes
140
+ * `{ spanType, operationId, callId, runtimeContext }` to the callback — no token
141
+ * usage and no resolved model — and its own attributes override custom keys. For
142
+ * `gen_ai.usage.cost.usd` on the model span, use `autotelTelemetry()` from
143
+ * `autotel-genai/observer` (it owns span creation), or price spans after the
144
+ * fact with {@link estimateAiSdkCost}. `autotel-devtools` also prices `gen_ai`
145
+ * spans on render regardless of which integration emitted them.
146
+ */
147
+ function autotelEnrich(options = {}) {
148
+ return (ctx) => ({
149
+ [AUTOTEL_ENRICHED_ATTR]: true,
150
+ ...options.attributes?.(ctx)
151
+ });
152
+ }
124
153
 
125
154
  //#endregion
126
- export { AI_SDK_ATTR, estimateAiSdkCost, extractAiSdkModel, extractAiSdkUsage, mapAiSdkAttributes, normalizeAiSdkProvider, recordAiSdkCost };
155
+ export { AI_SDK_ATTR, AUTOTEL_ENRICHED_ATTR, autotelEnrich, estimateAiSdkCost, extractAiSdkModel, extractAiSdkUsage, mapAiSdkAttributes, normalizeAiSdkProvider, recordAiSdkCost };
127
156
  //# sourceMappingURL=ai-sdk-bridge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-bridge.js","names":[],"sources":["../src/ai-sdk-bridge.ts"],"sourcesContent":["/**\n * Vercel AI SDK interop.\n *\n * The current Vercel AI SDK (`@ai-sdk/otel`'s `OpenTelemetry` integration,\n * stable since v7) already emits canonical `gen_ai.*` attributes and the\n * `invoke_agent {model}` › `chat {model}` › `execute_tool {tool}` span\n * hierarchy — so for new code there is nothing to map.\n *\n * This module exists for the two cases that still need help:\n *\n * 1. **Legacy `ai.*` attributes** — spans from `LegacyOpenTelemetry` or older\n * AI SDK versions. {@link mapAiSdkAttributes} rewrites them to `gen_ai.*`.\n * 2. **Cost enrichment** — neither integration emits cost. Pull usage from a\n * span's attributes (canonical *or* legacy) with {@link extractAiSdkUsage}\n * and price it, or copy a canonical `gen_ai.usage.cost.usd` onto your own\n * wrapping span with {@link recordAiSdkCost}.\n *\n * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry\n */\n\nimport type { TraceContext } from 'autotel';\nimport {\n genAiUsageAttributes,\n type GenAiAttributeMap,\n} from './attributes.js';\nimport {\n estimateLLMCost,\n type EstimateCostOptions,\n type TokenUsage,\n} from './cost.js';\nimport {\n GEN_AI,\n GEN_AI_PROVIDER,\n type GenAiProviderName,\n} from './semconv.js';\n\n/** Legacy AI SDK (`LegacyOpenTelemetry`) attribute keys we understand. */\nexport const AI_SDK_ATTR = {\n MODEL_ID: 'ai.model.id',\n MODEL_PROVIDER: 'ai.model.provider',\n RESPONSE_MODEL: 'ai.response.model',\n RESPONSE_ID: 'ai.response.id',\n RESPONSE_FINISH_REASON: 'ai.response.finishReason',\n USAGE_PROMPT_TOKENS: 'ai.usage.promptTokens',\n USAGE_INPUT_TOKENS: 'ai.usage.inputTokens',\n USAGE_COMPLETION_TOKENS: 'ai.usage.completionTokens',\n USAGE_OUTPUT_TOKENS: 'ai.usage.outputTokens',\n USAGE_CACHED_INPUT_TOKENS: 'ai.usage.cachedInputTokens',\n USAGE_REASONING_TOKENS: 'ai.usage.reasoningTokens',\n SETTINGS_MAX_TOKENS: 'ai.settings.maxOutputTokens',\n TELEMETRY_FUNCTION_ID: 'ai.telemetry.functionId',\n} as const;\n\nconst PROVIDER_PREFIX_MAP: Record<string, GenAiProviderName> = {\n openai: GEN_AI_PROVIDER.OPENAI,\n azure: GEN_AI_PROVIDER.AZURE_AI_OPENAI,\n anthropic: GEN_AI_PROVIDER.ANTHROPIC,\n google: GEN_AI_PROVIDER.GCP_GEMINI,\n 'google-vertex': GEN_AI_PROVIDER.GCP_VERTEX_AI,\n vertex: GEN_AI_PROVIDER.GCP_VERTEX_AI,\n 'amazon-bedrock': GEN_AI_PROVIDER.AWS_BEDROCK,\n bedrock: GEN_AI_PROVIDER.AWS_BEDROCK,\n cohere: GEN_AI_PROVIDER.COHERE,\n mistral: GEN_AI_PROVIDER.MISTRAL_AI,\n groq: GEN_AI_PROVIDER.GROQ,\n deepseek: GEN_AI_PROVIDER.DEEPSEEK,\n perplexity: GEN_AI_PROVIDER.PERPLEXITY,\n xai: GEN_AI_PROVIDER.X_AI,\n};\n\n/**\n * Normalize an AI SDK provider id (e.g. `openai.chat`, `amazon-bedrock`,\n * `google.generative-ai`) to a canonical `gen_ai.provider.name` value. Returns\n * the original string when it isn't a known provider.\n */\nexport function normalizeAiSdkProvider(provider: string): GenAiProviderName {\n const head = provider.split('.')[0]?.toLowerCase() ?? provider;\n return PROVIDER_PREFIX_MAP[head] ?? provider;\n}\n\nfunction num(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * Extract {@link TokenUsage} from a span's attributes, reading canonical\n * `gen_ai.usage.*` first and falling back to legacy `ai.usage.*`. Returns\n * `undefined` when no token counts are present.\n */\nexport function extractAiSdkUsage(\n attributes: Record<string, unknown>,\n): TokenUsage | undefined {\n const inputTokens =\n num(attributes[GEN_AI.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_PROMPT_TOKENS]);\n const outputTokens =\n num(attributes[GEN_AI.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_COMPLETION_TOKENS]);\n const cacheReadInputTokens =\n num(attributes[GEN_AI.USAGE_CACHE_READ_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_CACHED_INPUT_TOKENS]);\n const reasoningOutputTokens =\n num(attributes[GEN_AI.USAGE_REASONING_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_REASONING_TOKENS]);\n const cacheCreationInputTokens = num(\n attributes[GEN_AI.USAGE_CACHE_CREATION_INPUT_TOKENS],\n );\n\n if (\n inputTokens === undefined &&\n outputTokens === undefined &&\n cacheReadInputTokens === undefined &&\n reasoningOutputTokens === undefined &&\n cacheCreationInputTokens === undefined\n ) {\n return undefined;\n }\n return {\n inputTokens,\n outputTokens,\n reasoningOutputTokens,\n cacheReadInputTokens,\n cacheCreationInputTokens,\n };\n}\n\n/** Read the request model from canonical or legacy attributes. */\nexport function extractAiSdkModel(\n attributes: Record<string, unknown>,\n): string | undefined {\n return (\n str(attributes[GEN_AI.REQUEST_MODEL]) ?? str(attributes[AI_SDK_ATTR.MODEL_ID])\n );\n}\n\n/**\n * Rewrite legacy `ai.*` telemetry attributes to canonical `gen_ai.*`. Pass the\n * attributes of an AI SDK span emitted by `LegacyOpenTelemetry` (or an older\n * SDK version); returns a fresh map with the canonical keys. Unknown keys are\n * dropped — this is a focused mapper, not a passthrough.\n */\nexport function mapAiSdkAttributes(\n attributes: Record<string, unknown>,\n): GenAiAttributeMap {\n const out: GenAiAttributeMap = {};\n\n const model = str(attributes[AI_SDK_ATTR.MODEL_ID]);\n if (model) out[GEN_AI.REQUEST_MODEL] = model;\n\n const provider = str(attributes[AI_SDK_ATTR.MODEL_PROVIDER]);\n if (provider) out[GEN_AI.PROVIDER_NAME] = normalizeAiSdkProvider(provider);\n\n const responseModel = str(attributes[AI_SDK_ATTR.RESPONSE_MODEL]);\n if (responseModel) out[GEN_AI.RESPONSE_MODEL] = responseModel;\n\n const responseId = str(attributes[AI_SDK_ATTR.RESPONSE_ID]);\n if (responseId) out[GEN_AI.RESPONSE_ID] = responseId;\n\n const finishReason = str(attributes[AI_SDK_ATTR.RESPONSE_FINISH_REASON]);\n if (finishReason) out[GEN_AI.RESPONSE_FINISH_REASONS] = [finishReason];\n\n const maxTokens = num(attributes[AI_SDK_ATTR.SETTINGS_MAX_TOKENS]);\n if (maxTokens !== undefined) out[GEN_AI.REQUEST_MAX_TOKENS] = maxTokens;\n\n const functionId = str(attributes[AI_SDK_ATTR.TELEMETRY_FUNCTION_ID]);\n if (functionId) out[GEN_AI.AGENT_NAME] = functionId;\n\n const usage = extractAiSdkUsage(attributes);\n if (usage) Object.assign(out, genAiUsageAttributes(usage));\n\n return out;\n}\n\n/**\n * Estimate the USD cost of an AI SDK call from a span's attributes (model +\n * usage, canonical or legacy). Returns `undefined` when model or usage is\n * missing, or the model has no known pricing.\n */\nexport function estimateAiSdkCost(\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const model = extractAiSdkModel(attributes);\n const usage = extractAiSdkUsage(attributes);\n if (!model || !usage) return undefined;\n return estimateLLMCost(model, usage, options);\n}\n\n/**\n * Estimate cost from AI SDK span attributes and record it as\n * `gen_ai.usage.cost.usd` on your own wrapping trace context. Useful when you\n * wrap a `generateText`/`streamText` call in an autotel span and want cost on\n * the parent. Returns the estimated cost, or `undefined`.\n */\nexport function recordAiSdkCost(\n ctx: Pick<TraceContext, 'setAttribute'>,\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const cost = estimateAiSdkCost(attributes, options);\n if (cost !== undefined) ctx.setAttribute(GEN_AI.USAGE_COST_USD, cost);\n return cost;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai-sdk-bridge.js","names":[],"sources":["../src/ai-sdk-bridge.ts"],"sourcesContent":["/**\n * Vercel AI SDK interop.\n *\n * The current Vercel AI SDK (`@ai-sdk/otel`'s `OpenTelemetry` integration,\n * stable since v7) already emits canonical `gen_ai.*` attributes and the\n * `invoke_agent {model}` › `chat {model}` › `execute_tool {tool}` span\n * hierarchy — so for new code there is nothing to map.\n *\n * This module exists for the two cases that still need help:\n *\n * 1. **Legacy `ai.*` attributes** — spans from `LegacyOpenTelemetry` or older\n * AI SDK versions. {@link mapAiSdkAttributes} rewrites them to `gen_ai.*`.\n * 2. **Cost enrichment** — neither integration emits cost. Pull usage from a\n * span's attributes (canonical *or* legacy) with {@link extractAiSdkUsage}\n * and price it, or copy a canonical `gen_ai.usage.cost.usd` onto your own\n * wrapping span with {@link recordAiSdkCost}.\n *\n * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry\n */\n\nimport type { TraceContext } from 'autotel';\nimport {\n genAiUsageAttributes,\n type GenAiAttributeMap,\n} from './attributes.js';\nimport {\n estimateLLMCost,\n type EstimateCostOptions,\n type TokenUsage,\n} from './cost.js';\nimport {\n GEN_AI,\n GEN_AI_PROVIDER,\n type GenAiProviderName,\n} from './semconv.js';\n\n/** Legacy AI SDK (`LegacyOpenTelemetry`) attribute keys we understand. */\nexport const AI_SDK_ATTR = {\n MODEL_ID: 'ai.model.id',\n MODEL_PROVIDER: 'ai.model.provider',\n RESPONSE_MODEL: 'ai.response.model',\n RESPONSE_ID: 'ai.response.id',\n RESPONSE_FINISH_REASON: 'ai.response.finishReason',\n USAGE_PROMPT_TOKENS: 'ai.usage.promptTokens',\n USAGE_INPUT_TOKENS: 'ai.usage.inputTokens',\n USAGE_COMPLETION_TOKENS: 'ai.usage.completionTokens',\n USAGE_OUTPUT_TOKENS: 'ai.usage.outputTokens',\n USAGE_CACHED_INPUT_TOKENS: 'ai.usage.cachedInputTokens',\n USAGE_REASONING_TOKENS: 'ai.usage.reasoningTokens',\n SETTINGS_MAX_TOKENS: 'ai.settings.maxOutputTokens',\n TELEMETRY_FUNCTION_ID: 'ai.telemetry.functionId',\n} as const;\n\nconst PROVIDER_PREFIX_MAP: Record<string, GenAiProviderName> = {\n openai: GEN_AI_PROVIDER.OPENAI,\n azure: GEN_AI_PROVIDER.AZURE_AI_OPENAI,\n anthropic: GEN_AI_PROVIDER.ANTHROPIC,\n google: GEN_AI_PROVIDER.GCP_GEMINI,\n 'google-vertex': GEN_AI_PROVIDER.GCP_VERTEX_AI,\n vertex: GEN_AI_PROVIDER.GCP_VERTEX_AI,\n 'amazon-bedrock': GEN_AI_PROVIDER.AWS_BEDROCK,\n bedrock: GEN_AI_PROVIDER.AWS_BEDROCK,\n cohere: GEN_AI_PROVIDER.COHERE,\n mistral: GEN_AI_PROVIDER.MISTRAL_AI,\n groq: GEN_AI_PROVIDER.GROQ,\n deepseek: GEN_AI_PROVIDER.DEEPSEEK,\n perplexity: GEN_AI_PROVIDER.PERPLEXITY,\n xai: GEN_AI_PROVIDER.X_AI,\n};\n\n/**\n * Normalize an AI SDK provider id (e.g. `openai.chat`, `amazon-bedrock`,\n * `google.generative-ai`) to a canonical `gen_ai.provider.name` value. Returns\n * the original string when it isn't a known provider.\n */\nexport function normalizeAiSdkProvider(provider: string): GenAiProviderName {\n const head = provider.split('.')[0]?.toLowerCase() ?? provider;\n return PROVIDER_PREFIX_MAP[head] ?? provider;\n}\n\nfunction num(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * Extract {@link TokenUsage} from a span's attributes, reading canonical\n * `gen_ai.usage.*` first and falling back to legacy `ai.usage.*`. Returns\n * `undefined` when no token counts are present.\n */\nexport function extractAiSdkUsage(\n attributes: Record<string, unknown>,\n): TokenUsage | undefined {\n const inputTokens =\n num(attributes[GEN_AI.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_PROMPT_TOKENS]);\n const outputTokens =\n num(attributes[GEN_AI.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_COMPLETION_TOKENS]);\n const cacheReadInputTokens =\n num(attributes[GEN_AI.USAGE_CACHE_READ_INPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_CACHED_INPUT_TOKENS]);\n const reasoningOutputTokens =\n num(attributes[GEN_AI.USAGE_REASONING_OUTPUT_TOKENS]) ??\n num(attributes[AI_SDK_ATTR.USAGE_REASONING_TOKENS]);\n const cacheCreationInputTokens = num(\n attributes[GEN_AI.USAGE_CACHE_CREATION_INPUT_TOKENS],\n );\n\n if (\n inputTokens === undefined &&\n outputTokens === undefined &&\n cacheReadInputTokens === undefined &&\n reasoningOutputTokens === undefined &&\n cacheCreationInputTokens === undefined\n ) {\n return undefined;\n }\n return {\n inputTokens,\n outputTokens,\n reasoningOutputTokens,\n cacheReadInputTokens,\n cacheCreationInputTokens,\n };\n}\n\n/** Read the request model from canonical or legacy attributes. */\nexport function extractAiSdkModel(\n attributes: Record<string, unknown>,\n): string | undefined {\n return (\n str(attributes[GEN_AI.REQUEST_MODEL]) ?? str(attributes[AI_SDK_ATTR.MODEL_ID])\n );\n}\n\n/**\n * Rewrite legacy `ai.*` telemetry attributes to canonical `gen_ai.*`. Pass the\n * attributes of an AI SDK span emitted by `LegacyOpenTelemetry` (or an older\n * SDK version); returns a fresh map with the canonical keys. Unknown keys are\n * dropped — this is a focused mapper, not a passthrough.\n */\nexport function mapAiSdkAttributes(\n attributes: Record<string, unknown>,\n): GenAiAttributeMap {\n const out: GenAiAttributeMap = {};\n\n const model = str(attributes[AI_SDK_ATTR.MODEL_ID]);\n if (model) out[GEN_AI.REQUEST_MODEL] = model;\n\n const provider = str(attributes[AI_SDK_ATTR.MODEL_PROVIDER]);\n if (provider) out[GEN_AI.PROVIDER_NAME] = normalizeAiSdkProvider(provider);\n\n const responseModel = str(attributes[AI_SDK_ATTR.RESPONSE_MODEL]);\n if (responseModel) out[GEN_AI.RESPONSE_MODEL] = responseModel;\n\n const responseId = str(attributes[AI_SDK_ATTR.RESPONSE_ID]);\n if (responseId) out[GEN_AI.RESPONSE_ID] = responseId;\n\n const finishReason = str(attributes[AI_SDK_ATTR.RESPONSE_FINISH_REASON]);\n if (finishReason) out[GEN_AI.RESPONSE_FINISH_REASONS] = [finishReason];\n\n const maxTokens = num(attributes[AI_SDK_ATTR.SETTINGS_MAX_TOKENS]);\n if (maxTokens !== undefined) out[GEN_AI.REQUEST_MAX_TOKENS] = maxTokens;\n\n const functionId = str(attributes[AI_SDK_ATTR.TELEMETRY_FUNCTION_ID]);\n if (functionId) out[GEN_AI.AGENT_NAME] = functionId;\n\n const usage = extractAiSdkUsage(attributes);\n if (usage) Object.assign(out, genAiUsageAttributes(usage));\n\n return out;\n}\n\n/**\n * Estimate the USD cost of an AI SDK call from a span's attributes (model +\n * usage, canonical or legacy). Returns `undefined` when model or usage is\n * missing, or the model has no known pricing.\n */\nexport function estimateAiSdkCost(\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const model = extractAiSdkModel(attributes);\n const usage = extractAiSdkUsage(attributes);\n if (!model || !usage) return undefined;\n return estimateLLMCost(model, usage, options);\n}\n\n/**\n * Estimate cost from AI SDK span attributes and record it as\n * `gen_ai.usage.cost.usd` on your own wrapping trace context. Useful when you\n * wrap a `generateText`/`streamText` call in an autotel span and want cost on\n * the parent. Returns the estimated cost, or `undefined`.\n */\nexport function recordAiSdkCost(\n ctx: Pick<TraceContext, 'setAttribute'>,\n attributes: Record<string, unknown>,\n options?: EstimateCostOptions,\n): number | undefined {\n const cost = estimateAiSdkCost(attributes, options);\n if (cost !== undefined) ctx.setAttribute(GEN_AI.USAGE_COST_USD, cost);\n return cost;\n}\n\n// --- `@ai-sdk/otel` enrichSpan interop -------------------------------------\n\n/** Marks a span as having passed through an autotel-aware `enrichSpan`. */\nexport const AUTOTEL_ENRICHED_ATTR = 'autotel.enriched';\n\n/** Span kinds the `@ai-sdk/otel` `OpenTelemetry` integration emits. */\nexport type AiSdkSpanType =\n | 'operation'\n | 'step'\n | 'languageModel'\n | 'tool'\n | 'embedding'\n | 'reranking';\n\n/** The context `@ai-sdk/otel` passes to an `enrichSpan` callback. */\nexport interface AiSdkEnrichContext {\n spanType: AiSdkSpanType;\n operationId: string;\n callId: string;\n runtimeContext?: Record<string, unknown>;\n}\n\nexport interface AutotelEnrichOptions {\n /**\n * Map the enrich context to extra span attributes — e.g. promote\n * `runtimeContext` fields (sessionId, tenantId) onto the span. Returns\n * `undefined` to add nothing for that span.\n */\n attributes?: (\n ctx: AiSdkEnrichContext,\n ) => Record<string, string | number | boolean> | undefined;\n}\n\n/**\n * Build an `enrichSpan` callback for the `@ai-sdk/otel` `OpenTelemetry`\n * integration. It stamps an autotel provenance marker and merges any attributes\n * your `attributes` mapper returns:\n *\n * ```ts\n * import { registerTelemetry } from 'ai';\n * import { OpenTelemetry } from '@ai-sdk/otel';\n * import { autotelEnrich } from 'autotel-genai/ai-sdk';\n *\n * registerTelemetry(new OpenTelemetry({ enrichSpan: autotelEnrich() }));\n * ```\n *\n * Important: `enrichSpan` **cannot add cost**. The AI SDK only passes\n * `{ spanType, operationId, callId, runtimeContext }` to the callback — no token\n * usage and no resolved model — and its own attributes override custom keys. For\n * `gen_ai.usage.cost.usd` on the model span, use `autotelTelemetry()` from\n * `autotel-genai/observer` (it owns span creation), or price spans after the\n * fact with {@link estimateAiSdkCost}. `autotel-devtools` also prices `gen_ai`\n * spans on render regardless of which integration emitted them.\n */\nexport function autotelEnrich(\n options: AutotelEnrichOptions = {},\n): (ctx: AiSdkEnrichContext) => Record<string, string | number | boolean> {\n return (ctx) => ({\n [AUTOTEL_ENRICHED_ATTR]: true,\n ...options.attributes?.(ctx),\n });\n}\n"],"mappings":";;;;;;AAqCA,MAAa,cAAc;CACzB,UAAU;CACV,gBAAgB;CAChB,gBAAgB;CAChB,aAAa;CACb,wBAAwB;CACxB,qBAAqB;CACrB,oBAAoB;CACpB,yBAAyB;CACzB,qBAAqB;CACrB,2BAA2B;CAC3B,wBAAwB;CACxB,qBAAqB;CACrB,uBAAuB;AACzB;AAEA,MAAM,sBAAyD;CAC7D,QAAQ,gBAAgB;CACxB,OAAO,gBAAgB;CACvB,WAAW,gBAAgB;CAC3B,QAAQ,gBAAgB;CACxB,iBAAiB,gBAAgB;CACjC,QAAQ,gBAAgB;CACxB,kBAAkB,gBAAgB;CAClC,SAAS,gBAAgB;CACzB,QAAQ,gBAAgB;CACxB,SAAS,gBAAgB;CACzB,MAAM,gBAAgB;CACtB,UAAU,gBAAgB;CAC1B,YAAY,gBAAgB;CAC5B,KAAK,gBAAgB;AACvB;;;;;;AAOA,SAAgB,uBAAuB,UAAqC;CAE1E,OAAO,oBADM,SAAS,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,YAAY,KAAK,aAClB;AACtC;AAEA,SAAS,IAAI,OAAoC;CAC/C,OAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAEA,SAAS,IAAI,OAAoC;CAC/C,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;;;;;;AAOA,SAAgB,kBACd,YACwB;CACxB,MAAM,cACJ,IAAI,WAAW,OAAO,mBAAmB,KACzC,IAAI,WAAW,YAAY,mBAAmB,KAC9C,IAAI,WAAW,YAAY,oBAAoB;CACjD,MAAM,eACJ,IAAI,WAAW,OAAO,oBAAoB,KAC1C,IAAI,WAAW,YAAY,oBAAoB,KAC/C,IAAI,WAAW,YAAY,wBAAwB;CACrD,MAAM,uBACJ,IAAI,WAAW,OAAO,8BAA8B,KACpD,IAAI,WAAW,YAAY,0BAA0B;CACvD,MAAM,wBACJ,IAAI,WAAW,OAAO,8BAA8B,KACpD,IAAI,WAAW,YAAY,uBAAuB;CACpD,MAAM,2BAA2B,IAC/B,WAAW,OAAO,kCACpB;CAEA,IACE,gBAAgB,UAChB,iBAAiB,UACjB,yBAAyB,UACzB,0BAA0B,UAC1B,6BAA6B,QAE7B;CAEF,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;AACF;;AAGA,SAAgB,kBACd,YACoB;CACpB,OACE,IAAI,WAAW,OAAO,cAAc,KAAK,IAAI,WAAW,YAAY,SAAS;AAEjF;;;;;;;AAQA,SAAgB,mBACd,YACmB;CACnB,MAAM,MAAyB,CAAC;CAEhC,MAAM,QAAQ,IAAI,WAAW,YAAY,SAAS;CAClD,IAAI,OAAO,IAAI,OAAO,iBAAiB;CAEvC,MAAM,WAAW,IAAI,WAAW,YAAY,eAAe;CAC3D,IAAI,UAAU,IAAI,OAAO,iBAAiB,uBAAuB,QAAQ;CAEzE,MAAM,gBAAgB,IAAI,WAAW,YAAY,eAAe;CAChE,IAAI,eAAe,IAAI,OAAO,kBAAkB;CAEhD,MAAM,aAAa,IAAI,WAAW,YAAY,YAAY;CAC1D,IAAI,YAAY,IAAI,OAAO,eAAe;CAE1C,MAAM,eAAe,IAAI,WAAW,YAAY,uBAAuB;CACvE,IAAI,cAAc,IAAI,OAAO,2BAA2B,CAAC,YAAY;CAErE,MAAM,YAAY,IAAI,WAAW,YAAY,oBAAoB;CACjE,IAAI,cAAc,QAAW,IAAI,OAAO,sBAAsB;CAE9D,MAAM,aAAa,IAAI,WAAW,YAAY,sBAAsB;CACpE,IAAI,YAAY,IAAI,OAAO,cAAc;CAEzC,MAAM,QAAQ,kBAAkB,UAAU;CAC1C,IAAI,OAAO,OAAO,OAAO,KAAK,qBAAqB,KAAK,CAAC;CAEzD,OAAO;AACT;;;;;;AAOA,SAAgB,kBACd,YACA,SACoB;CACpB,MAAM,QAAQ,kBAAkB,UAAU;CAC1C,MAAM,QAAQ,kBAAkB,UAAU;CAC1C,IAAI,CAAC,SAAS,CAAC,OAAO,OAAO;CAC7B,OAAO,gBAAgB,OAAO,OAAO,OAAO;AAC9C;;;;;;;AAQA,SAAgB,gBACd,KACA,YACA,SACoB;CACpB,MAAM,OAAO,kBAAkB,YAAY,OAAO;CAClD,IAAI,SAAS,QAAW,IAAI,aAAa,OAAO,gBAAgB,IAAI;CACpE,OAAO;AACT;;AAKA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;AAmDrC,SAAgB,cACd,UAAgC,CAAC,GACuC;CACxE,QAAQ,SAAS;GACd,wBAAwB;EACzB,GAAG,QAAQ,aAAa,GAAG;CAC7B;AACF"}