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 +105 -4
- package/dist/ai-sdk-bridge.cjs +31 -0
- package/dist/ai-sdk-bridge.cjs.map +1 -1
- package/dist/ai-sdk-bridge.d.cts +42 -1
- package/dist/ai-sdk-bridge.d.cts.map +1 -1
- package/dist/ai-sdk-bridge.d.ts +42 -1
- package/dist/ai-sdk-bridge.d.ts.map +1 -1
- package/dist/ai-sdk-bridge.js +30 -1
- package/dist/ai-sdk-bridge.js.map +1 -1
- package/dist/{index-CIkLg2LA.d.ts → index-BiTlgIvW.d.ts} +255 -2
- package/dist/index-BiTlgIvW.d.ts.map +1 -0
- package/dist/{index-CGHFYLGQ.d.cts → index-BkYnyG--.d.cts} +255 -2
- package/dist/index-BkYnyG--.d.cts.map +1 -0
- package/dist/index.cjs +7 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/observer/index.cjs +6 -2
- package/dist/observer/index.d.cts +2 -2
- package/dist/observer/index.d.ts +2 -2
- package/dist/observer/index.js +2 -2
- package/dist/observer-CNaW3fb1.js +1060 -0
- package/dist/observer-CNaW3fb1.js.map +1 -0
- package/dist/{observer-BbhXgZBA.cjs → observer-YBI0hQqC.cjs} +551 -14
- package/dist/observer-YBI0hQqC.cjs.map +1 -0
- package/package.json +3 -3
- package/skills/autotel-genai/SKILL.md +25 -3
- package/dist/index-CGHFYLGQ.d.cts.map +0 -1
- package/dist/index-CIkLg2LA.d.ts.map +0 -1
- package/dist/observer-50RGrqzo.js +0 -547
- package/dist/observer-50RGrqzo.js.map +0 -1
- package/dist/observer-BbhXgZBA.cjs.map +0 -1
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/
|
|
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
|
-
|
|
205
|
-
`
|
|
206
|
-
|
|
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
|
package/dist/ai-sdk-bridge.cjs
CHANGED
|
@@ -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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"}
|
package/dist/ai-sdk-bridge.d.cts
CHANGED
|
@@ -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"}
|
package/dist/ai-sdk-bridge.d.ts
CHANGED
|
@@ -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"}
|
package/dist/ai-sdk-bridge.js
CHANGED
|
@@ -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"}
|