@respan/instrumentation-vercel 1.0.3 → 1.0.5

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.
@@ -8,603 +8,12 @@
8
8
  * Two-phase enrichment:
9
9
  * - onStart(): Sets RESPAN_LOG_TYPE so the span passes CompositeProcessor filtering
10
10
  * - onEnd(): Full attribute translation (model, messages, tokens, metadata, etc.)
11
- *
12
- * Vercel attrs are preserved (additive enrichment via setDefault, not destructive).
13
- *
14
- * Ported from @respan/exporter-vercel — all exporter features are replicated:
15
- * - Model normalization (Gemini, Claude, DeepSeek, O3-mini)
16
- * - Prompt message parsing (ai.prompt.messages + ai.prompt fallback)
17
- * - Completion message building (ai.response.text, ai.response.object, tool calls)
18
- * - Token count normalization (input/output → prompt/completion)
19
- * - Tool definitions (ai.prompt.tools) and tool choice (ai.prompt.toolChoice)
20
- * - Customer params (ai.telemetry.metadata.customer_* + customer_params JSON)
21
- * - General metadata (ai.telemetry.metadata.* → respan.metadata.*)
22
- * - Stream detection, environment, cost, TTFT, generation time, unit prices
23
- * - Log type detection with operationId + attribute-based fallbacks
24
- */
25
- import { RespanSpanAttributes, RespanLogType } from "@respan/respan-sdk";
26
- import { VERCEL_SPAN_CONFIG, VERCEL_PARENT_SPANS } from "./constants/index.js";
27
- // ── Attribute keys (single source of truth from SDK) ─────────────────────────
28
- const RESPAN_LOG_TYPE = RespanSpanAttributes.RESPAN_LOG_TYPE;
29
- const GEN_AI_REQUEST_MODEL = RespanSpanAttributes.GEN_AI_REQUEST_MODEL;
30
- const GEN_AI_USAGE_PROMPT_TOKENS = RespanSpanAttributes.GEN_AI_USAGE_PROMPT_TOKENS;
31
- const GEN_AI_USAGE_COMPLETION_TOKENS = RespanSpanAttributes.GEN_AI_USAGE_COMPLETION_TOKENS;
32
- const LLM_REQUEST_TYPE = RespanSpanAttributes.LLM_REQUEST_TYPE;
33
- const CUSTOMER_ID = RespanSpanAttributes.RESPAN_CUSTOMER_PARAMS_ID;
34
- const CUSTOMER_EMAIL = RespanSpanAttributes.RESPAN_CUSTOMER_PARAMS_EMAIL;
35
- const CUSTOMER_NAME = RespanSpanAttributes.RESPAN_CUSTOMER_PARAMS_NAME;
36
- const THREAD_ID = RespanSpanAttributes.RESPAN_THREADS_ID;
37
- // RESPAN_SESSION_ID not yet published in @respan/respan-sdk — use wire value directly
38
- const SESSION_ID = "respan.sessions.session_identifier";
39
- const TRACE_GROUP_ID = RespanSpanAttributes.RESPAN_TRACE_GROUP_ID;
40
- const RESPAN_SPAN_TOOLS = RespanSpanAttributes.RESPAN_SPAN_TOOLS;
41
- const RESPAN_METADATA_AGENT_NAME = RespanSpanAttributes.RESPAN_METADATA_AGENT_NAME;
42
- const RESPAN_METADATA_PREFIX = RespanSpanAttributes.RESPAN_METADATA; // "respan.metadata"
43
- /** Build a respan.metadata.<key> attribute name. */
44
- function metadataKey(key) {
45
- return `${RESPAN_METADATA_PREFIX}.${key}`;
46
- }
47
- // Traceloop wire-format keys
48
- const TL_SPAN_KIND = "traceloop.span.kind";
49
- const TL_ENTITY_NAME = "traceloop.entity.name";
50
- const TL_ENTITY_INPUT = "traceloop.entity.input";
51
- const TL_ENTITY_OUTPUT = "traceloop.entity.output";
52
- const TL_ENTITY_PATH = "traceloop.entity.path";
53
- // ── Helpers ──────────────────────────────────────────────────────────────────
54
- function setDefault(attrs, key, value) {
55
- if (attrs[key] === undefined && value !== undefined && value !== null) {
56
- attrs[key] = value;
57
- }
58
- }
59
- function safeJsonStr(value) {
60
- if (value === undefined || value === null)
61
- return "";
62
- if (typeof value === "string")
63
- return value;
64
- try {
65
- return JSON.stringify(value);
66
- }
67
- catch {
68
- return String(value);
69
- }
70
- }
71
- function safeJsonParse(value) {
72
- if (typeof value !== "string")
73
- return value;
74
- try {
75
- return JSON.parse(value);
76
- }
77
- catch {
78
- return value;
79
- }
80
- }
81
- /**
82
- * Detect whether a span is from the Vercel AI SDK.
83
- *
84
- * Primary check: instrumentation scope name === "ai" (set by the Vercel AI SDK).
85
- * Fallback: ai.sdk attribute or ai.* span name.
86
- *
87
- * Does NOT match on gen_ai.* attributes alone — those may come from other
88
- * instrumentations (OpenInference, Traceloop) and must not be stripped.
89
- */
90
- function isVercelAISpan(span) {
91
- // Primary: check OTEL instrumentation scope (most reliable, no false positives)
92
- if (span.instrumentationLibrary?.name === "ai")
93
- return true;
94
- // Fallback: explicit Vercel marker or span name convention
95
- if (span.attributes["ai.sdk"] !== undefined)
96
- return true;
97
- if (span.name.startsWith("ai."))
98
- return true;
99
- return false;
100
- }
101
- // ── Log type detection (with operationId + attribute fallbacks) ──────────────
102
- /**
103
- * Resolve the Respan log type for a Vercel AI SDK span.
104
- * Replicates the full fallback chain from the exporter's parseLogType().
105
- */
106
- function resolveLogType(name, attrs) {
107
- // 1. Direct span name match
108
- const config = VERCEL_SPAN_CONFIG[name];
109
- if (config)
110
- return config.logType;
111
- const parentType = VERCEL_PARENT_SPANS[name];
112
- if (parentType)
113
- return parentType;
114
- // 2. operationId attribute fallback
115
- const operationId = attrs["ai.operationId"]?.toString();
116
- if (operationId) {
117
- const opConfig = VERCEL_SPAN_CONFIG[operationId];
118
- if (opConfig)
119
- return opConfig.logType;
120
- const opParent = VERCEL_PARENT_SPANS[operationId];
121
- if (opParent)
122
- return opParent;
123
- }
124
- // 3. Attribute-based fallback detection (same heuristics as exporter)
125
- if (attrs["ai.embedding"] || attrs["ai.embeddings"] ||
126
- name.includes("embed") || operationId?.includes("embed")) {
127
- return RespanLogType.EMBEDDING;
128
- }
129
- if (attrs["ai.toolCall.id"] || attrs["ai.toolCall.name"] ||
130
- attrs["ai.toolCall.args"] || attrs["ai.toolCall.result"] ||
131
- attrs["ai.response.toolCalls"] ||
132
- name.includes("tool") || operationId?.includes("tool")) {
133
- return RespanLogType.TOOL;
134
- }
135
- if (attrs["ai.agent.id"] ||
136
- name.includes("agent") || operationId?.includes("agent")) {
137
- return RespanLogType.AGENT;
138
- }
139
- if (attrs["ai.workflow.id"] ||
140
- name.includes("workflow") || operationId?.includes("workflow")) {
141
- return RespanLogType.WORKFLOW;
142
- }
143
- if (attrs["ai.transcript"] ||
144
- name.includes("transcript") || operationId?.includes("transcript")) {
145
- return RespanLogType.TRANSCRIPTION;
146
- }
147
- if (attrs["ai.speech"] ||
148
- name.includes("speech") || operationId?.includes("speech")) {
149
- return RespanLogType.SPEECH;
150
- }
151
- // 4. Generation span fallback (doGenerate/doStream)
152
- if (name.includes("doGenerate") || name.includes("doStream")) {
153
- return RespanLogType.TEXT;
154
- }
155
- return RespanLogType.UNKNOWN;
156
- }
157
- // ── Model normalization ──────────────────────────────────────────────────────
158
- /**
159
- * Normalize the model ID from Vercel's ai.model.id to a standard model name.
160
- * Replicates the logic from the existing exporter.
161
- */
162
- function normalizeModel(modelId) {
163
- const model = modelId.toLowerCase();
164
- if (model.includes("gemini-2.0-flash-001"))
165
- return "gemini/gemini-2.0-flash";
166
- if (model.includes("gemini-2.0-pro"))
167
- return "gemini/gemini-2.0-pro-exp-02-05";
168
- if (model.includes("claude-3-5-sonnet"))
169
- return "claude-3-5-sonnet-20241022";
170
- if (model.includes("deepseek"))
171
- return "deepseek/" + model;
172
- if (model.includes("o3-mini"))
173
- return "o3-mini";
174
- return model;
175
- }
176
- // ── Prompt/completion message formatting ─────────────────────────────────────
177
- /**
178
- * Parse ai.prompt.messages into a JSON string suitable for traceloop.entity.input.
179
- * Falls back to ai.prompt as a single user message.
180
11
  */
181
- function formatPromptInput(attrs) {
182
- const messages = attrs["ai.prompt.messages"];
183
- if (messages) {
184
- try {
185
- const parsed = typeof messages === "string" ? JSON.parse(messages) : messages;
186
- return safeJsonStr(parsed);
187
- }
188
- catch {
189
- // fall through
190
- }
191
- }
192
- const prompt = attrs["ai.prompt"];
193
- if (prompt) {
194
- return safeJsonStr([{ role: "user", content: String(prompt) }]);
195
- }
196
- return undefined;
197
- }
198
- /**
199
- * Build completion output from ai.response.text, ai.response.object, and tool calls.
200
- * Also includes tool result messages when present (for tool call spans).
201
- */
202
- function formatCompletionOutput(attrs) {
203
- let content = "";
204
- if (attrs["ai.response.object"]) {
205
- try {
206
- const rawObject = attrs["ai.response.object"];
207
- const parsed = typeof rawObject === "string" ? JSON.parse(rawObject) : rawObject;
208
- // generateObject returns the object directly (no `response` wrapper).
209
- // Prefer known wrappers when present, otherwise serialize the object itself.
210
- const normalized = parsed?.response ?? parsed?.object ?? parsed?.output ?? parsed?.result ?? parsed;
211
- content = safeJsonStr(normalized);
212
- }
213
- catch {
214
- content = String(attrs["ai.response.text"] ?? "");
215
- }
216
- }
217
- else {
218
- content = String(attrs["ai.response.text"] ?? "");
219
- }
220
- // Build assistant message with optional tool calls
221
- const toolCalls = parseToolCalls(attrs);
222
- // Bail only when there's no text AND no tool calls
223
- if (!content && (!toolCalls || toolCalls.length === 0))
224
- return undefined;
225
- const message = { role: "assistant", content };
226
- if (toolCalls && toolCalls.length > 0) {
227
- message.tool_calls = toolCalls;
228
- }
229
- // Include tool result as a separate message if present
230
- const messages = [message];
231
- if (attrs["ai.toolCall.result"]) {
232
- messages.push({
233
- role: "tool",
234
- tool_call_id: String(attrs["ai.toolCall.id"] || ""),
235
- content: String(attrs["ai.toolCall.result"] || ""),
236
- });
237
- }
238
- return safeJsonStr(messages.length === 1 ? message : messages);
239
- }
240
- /**
241
- * Parse tool call data from various Vercel AI SDK attribute formats.
242
- */
243
- function parseToolCalls(attrs) {
244
- // Try array formats first
245
- for (const key of ["ai.response.toolCalls", "ai.toolCall", "ai.toolCalls"]) {
246
- if (!attrs[key])
247
- continue;
248
- try {
249
- const parsed = typeof attrs[key] === "string" ? JSON.parse(attrs[key]) : attrs[key];
250
- const calls = Array.isArray(parsed) ? parsed : [parsed];
251
- return calls.map((call) => {
252
- if (!call || typeof call !== "object")
253
- return { type: "function" };
254
- const result = { ...call };
255
- if (!result.type)
256
- result.type = "function";
257
- if (!result.id && (result.toolCallId || result.tool_call_id)) {
258
- result.id = result.toolCallId || result.tool_call_id;
259
- }
260
- return result;
261
- });
262
- }
263
- catch {
264
- continue;
265
- }
266
- }
267
- // Try individual tool call attributes
268
- if (attrs["ai.toolCall.id"] || attrs["ai.toolCall.name"] || attrs["ai.toolCall.args"]) {
269
- const toolCall = { type: "function" };
270
- for (const [key, value] of Object.entries(attrs)) {
271
- if (key.startsWith("ai.toolCall.")) {
272
- toolCall[key.replace("ai.toolCall.", "")] = value;
273
- }
274
- }
275
- return [toolCall];
276
- }
277
- return undefined;
278
- }
279
- /**
280
- * Format tool call span input/output for traceloop.entity.input/output.
281
- */
282
- function formatToolInput(attrs) {
283
- const name = attrs["ai.toolCall.name"];
284
- const args = attrs["ai.toolCall.args"];
285
- if (!name && !args)
286
- return undefined;
287
- const input = {};
288
- if (name)
289
- input.name = name;
290
- if (args) {
291
- input.args = typeof args === "string" ? safeJsonParse(args) : args;
292
- }
293
- return safeJsonStr(input);
294
- }
295
- function formatToolOutput(attrs) {
296
- const result = attrs["ai.toolCall.result"];
297
- if (result === undefined)
298
- return undefined;
299
- return safeJsonStr(typeof result === "string" ? safeJsonParse(result) : result);
300
- }
301
- // ── Tools & tool choice (from exporter's parseTools / parseToolChoice) ───────
302
- /**
303
- * Parse ai.prompt.tools into a normalized tool definition array.
304
- * Accepts both nested ({type:"function",function:{...}}) and flat shapes.
305
- */
306
- function parseTools(attrs) {
307
- try {
308
- const tools = attrs["ai.prompt.tools"];
309
- if (!tools)
310
- return undefined;
311
- const raw = Array.isArray(tools) ? tools : [tools];
312
- const parsed = raw
313
- .map((tool) => {
314
- try {
315
- return typeof tool === "string" ? JSON.parse(tool) : tool;
316
- }
317
- catch {
318
- return undefined;
319
- }
320
- })
321
- .filter(Boolean)
322
- .map((tool) => {
323
- // Accept both nested and flat shapes; normalize to nested
324
- if (tool && tool.type === "function") {
325
- if (tool.function && typeof tool.function === "object") {
326
- // Already nested — move top-level inputSchema into function.parameters
327
- // (Vercel AI SDK puts inputSchema at the top level, backend expects function.parameters)
328
- if (tool.inputSchema && !tool.function.parameters) {
329
- const { inputSchema, ...rest } = tool;
330
- return { ...rest, function: { ...tool.function, parameters: inputSchema } };
331
- }
332
- return tool;
333
- }
334
- const { name, description, parameters, inputSchema, ...rest } = tool;
335
- const params = parameters ?? inputSchema;
336
- return {
337
- ...rest,
338
- type: "function",
339
- function: {
340
- name,
341
- ...(description ? { description } : {}),
342
- ...(params ? { parameters: params } : {}),
343
- },
344
- };
345
- }
346
- return tool;
347
- });
348
- if (parsed.length === 0)
349
- return undefined;
350
- return safeJsonStr(parsed);
351
- }
352
- catch {
353
- return undefined;
354
- }
355
- }
356
- /**
357
- * Parse tool choice from Vercel's ai.prompt.toolChoice attribute.
358
- */
359
- function parseToolChoice(attrs) {
360
- try {
361
- const toolChoice = attrs["ai.prompt.toolChoice"];
362
- if (!toolChoice)
363
- return undefined;
364
- const parsed = typeof toolChoice === "string" ? JSON.parse(toolChoice) : toolChoice;
365
- if (parsed.function?.name) {
366
- return safeJsonStr({
367
- type: String(parsed.type),
368
- function: { name: String(parsed.function.name) },
369
- });
370
- }
371
- return safeJsonStr({ type: String(parsed.type) });
372
- }
373
- catch {
374
- return undefined;
375
- }
376
- }
377
- // ── Metadata / customer params ───────────────────────────────────────────────
378
- /**
379
- * Extract ai.telemetry.metadata.* and map customer/thread params to Respan attrs.
380
- * Also handles prompt_unit_price, completion_unit_price, and span_type metadata.
381
- */
382
- function enrichMetadata(attrs, spanName) {
383
- for (const [key, value] of Object.entries(attrs)) {
384
- if (!key.startsWith("ai.telemetry.metadata."))
385
- continue;
386
- const cleanKey = key.slice("ai.telemetry.metadata.".length);
387
- // Map well-known keys to Respan span attributes
388
- switch (cleanKey) {
389
- case "customer_identifier":
390
- setDefault(attrs, CUSTOMER_ID, String(value));
391
- break;
392
- case "customer_email":
393
- setDefault(attrs, CUSTOMER_EMAIL, String(value));
394
- break;
395
- case "customer_name":
396
- setDefault(attrs, CUSTOMER_NAME, String(value));
397
- break;
398
- case "session_identifier":
399
- setDefault(attrs, SESSION_ID, String(value));
400
- break;
401
- case "thread_identifier":
402
- setDefault(attrs, THREAD_ID, String(value));
403
- break;
404
- case "trace_group_identifier":
405
- setDefault(attrs, TRACE_GROUP_ID, String(value));
406
- break;
407
- case "customer_params": {
408
- // customer_params can be a JSON object with all three fields
409
- try {
410
- const parsed = typeof value === "string" ? JSON.parse(value) : value;
411
- if (parsed?.customer_identifier)
412
- setDefault(attrs, CUSTOMER_ID, parsed.customer_identifier);
413
- if (parsed?.customer_email)
414
- setDefault(attrs, CUSTOMER_EMAIL, parsed.customer_email);
415
- if (parsed?.customer_name)
416
- setDefault(attrs, CUSTOMER_NAME, parsed.customer_name);
417
- }
418
- catch {
419
- // ignore
420
- }
421
- break;
422
- }
423
- case "prompt_unit_price":
424
- setDefault(attrs, metadataKey("prompt_unit_price"), String(value));
425
- break;
426
- case "completion_unit_price":
427
- setDefault(attrs, metadataKey("completion_unit_price"), String(value));
428
- break;
429
- case "userId":
430
- // userId is a fallback for customer_identifier (backward compat with exporter)
431
- setDefault(attrs, CUSTOMER_ID, String(value));
432
- setDefault(attrs, metadataKey(cleanKey), String(value ?? ""));
433
- break;
434
- default:
435
- // All other metadata → respan.metadata.<key>
436
- setDefault(attrs, metadataKey(cleanKey), String(value ?? ""));
437
- break;
438
- }
439
- }
440
- }
441
- // ── Token count normalization ────────────────────────────────────────────────
442
- function enrichTokens(attrs) {
443
- // Vercel AI SDK may use gen_ai.usage.input_tokens / gen_ai.usage.output_tokens
444
- // Respan backend expects gen_ai.usage.prompt_tokens / gen_ai.usage.completion_tokens
445
- const inputTokens = attrs["gen_ai.usage.input_tokens"] ??
446
- attrs["gen_ai.usage.prompt_tokens"];
447
- const outputTokens = attrs["gen_ai.usage.output_tokens"] ??
448
- attrs["gen_ai.usage.completion_tokens"];
449
- if (inputTokens !== undefined) {
450
- setDefault(attrs, GEN_AI_USAGE_PROMPT_TOKENS, Number(inputTokens));
451
- }
452
- if (outputTokens !== undefined) {
453
- setDefault(attrs, GEN_AI_USAGE_COMPLETION_TOKENS, Number(outputTokens));
454
- }
455
- }
456
- // ── Performance / cost metrics ───────────────────────────────────────────────
457
- /**
458
- * Enrich performance and cost attributes that the exporter handled explicitly.
459
- * These are Vercel-specific attrs that the backend needs in standard locations.
460
- */
461
- function enrichPerformanceMetrics(attrs, spanName) {
462
- // Stream detection from span name
463
- setDefault(attrs, metadataKey("stream"), String(spanName.includes("doStream")));
464
- // Time to first token from ai.response.msToFinish (Vercel-specific)
465
- const msToFinish = attrs["ai.response.msToFinish"];
466
- if (msToFinish !== undefined) {
467
- setDefault(attrs, metadataKey("time_to_first_token"), String(Number(msToFinish) / 1000));
468
- }
469
- // Cost (gen_ai.usage.cost is standard but ensure it's present)
470
- const cost = attrs["gen_ai.usage.cost"];
471
- if (cost !== undefined) {
472
- setDefault(attrs, metadataKey("cost"), String(cost));
473
- }
474
- // TTFT (gen_ai.usage.ttft)
475
- const ttft = attrs["gen_ai.usage.ttft"];
476
- if (ttft !== undefined) {
477
- setDefault(attrs, metadataKey("ttft"), String(ttft));
478
- }
479
- // Generation time
480
- const genTime = attrs["gen_ai.usage.generation_time"];
481
- if (genTime !== undefined) {
482
- setDefault(attrs, metadataKey("generation_time"), String(genTime));
483
- }
484
- // Warnings
485
- const warnings = attrs["gen_ai.usage.warnings"];
486
- if (warnings !== undefined) {
487
- setDefault(attrs, metadataKey("warnings"), String(warnings));
488
- }
489
- // Response type (text/json_schema/json_object)
490
- const type = attrs["gen_ai.usage.type"];
491
- if (type !== undefined) {
492
- setDefault(attrs, metadataKey("type"), String(type));
493
- }
494
- }
495
- // ── Cleanup: strip redundant Vercel attrs after translation ──────────────────
496
- /**
497
- * Vercel AI SDK attributes that have been translated to Traceloop/GenAI/Respan
498
- * equivalents. These are deleted after translation to keep spans clean.
499
- */
500
- const VERCEL_ATTRS_TO_STRIP = [
501
- // ── Vercel AI SDK attrs (translated to Traceloop/GenAI equivalents) ────
502
- // Model (translated to gen_ai.request.model)
503
- "ai.model.id",
504
- "ai.model.provider",
505
- "ai.response.model",
506
- // Prompt/completion (translated to traceloop.entity.input/output)
507
- "ai.prompt",
508
- "ai.prompt.messages",
509
- "ai.prompt.format",
510
- "ai.response.text",
511
- "ai.response.object",
512
- // Tokens — old names (v5) + new names (v6)
513
- "ai.usage.promptTokens",
514
- "ai.usage.completionTokens",
515
- "ai.usage.inputTokens",
516
- "ai.usage.outputTokens",
517
- "ai.usage.totalTokens",
518
- "ai.usage.reasoningTokens",
519
- "ai.usage.cachedInputTokens",
520
- // Response metadata (redundant with standard OTEL/GenAI attrs)
521
- "ai.response.finishReason",
522
- "ai.response.id",
523
- "ai.response.timestamp",
524
- "ai.response.providerMetadata",
525
- "ai.response.msToFinish",
526
- "ai.response.msToFirstChunk",
527
- "ai.response.avgOutputTokensPerSecond",
528
- "ai.response.avgCompletionTokensPerSecond",
529
- // Request metadata
530
- "ai.request.headers.user-agent",
531
- // Tool choice (translated to respan.metadata.tool_choice)
532
- "ai.prompt.toolChoice",
533
- // SDK internals (no user value)
534
- "ai.operationId",
535
- "ai.settings.maxRetries",
536
- "ai.settings.maxSteps",
537
- "ai.sdk",
538
- "operation.name",
539
- // Tool calls (translated to traceloop.entity.input/output for tool spans)
540
- "ai.toolCall.id",
541
- "ai.toolCall.name",
542
- "ai.toolCall.args",
543
- "ai.toolCall.result",
544
- "ai.response.toolCalls",
545
- // GenAI duplicates (already consumed by backend as top-level fields)
546
- "gen_ai.response.finish_reasons",
547
- "gen_ai.response.id",
548
- "gen_ai.usage.input_tokens",
549
- "gen_ai.usage.output_tokens",
550
- "gen_ai.system",
551
- // ── Traceloop routing attrs (Vercel-specific, not user-facing) ──────────
552
- // Keep traceloop.span.kind and respan.entity.log_type — backend needs them.
553
- // Keep respan.environment — may be set by user via propagateAttributes().
554
- "traceloop.entity.name",
555
- "traceloop.entity.path",
556
- // ── OTEL resource / process noise (no user value in metadata) ──────────
557
- "service.name",
558
- "telemetry.sdk.language",
559
- "telemetry.sdk.name",
560
- "telemetry.sdk.version",
561
- "process.pid",
562
- "process.executable.name",
563
- "process.executable.path",
564
- "process.command_args",
565
- "process.runtime.version",
566
- "process.runtime.name",
567
- "process.runtime.description",
568
- "process.command",
569
- "process.owner",
570
- "host.name",
571
- "host.arch",
572
- "host.id",
573
- "otel.scope.name",
574
- "otel.scope.version",
575
- // ── Next.js auto-instrumentation noise ─────────────────────────────────
576
- "next.span_name",
577
- "next.span_type",
578
- "http.url",
579
- "http.method",
580
- "net.peer.name",
581
- ];
582
- /**
583
- * Remove redundant Vercel AI SDK attributes after translation.
584
- * Also strips ai.telemetry.metadata.* keys that have been mapped to respan.* attrs.
585
- */
586
- function stripRedundantAttrs(attrs) {
587
- for (const key of VERCEL_ATTRS_TO_STRIP) {
588
- delete attrs[key];
589
- }
590
- for (const key of Object.keys(attrs)) {
591
- // Strip ai.telemetry.metadata.* (already mapped to respan.metadata.* / respan.customer_params.*)
592
- if (key.startsWith("ai.telemetry.metadata.")) {
593
- delete attrs[key];
594
- continue;
595
- }
596
- // Strip ai.usage.*Details.* (e.g. inputTokenDetails.noCacheTokens, outputTokenDetails.textTokens)
597
- if (key.startsWith("ai.usage.") && key.includes("Details.")) {
598
- delete attrs[key];
599
- continue;
600
- }
601
- }
602
- // Strip ai.prompt.tools (translated to respan.span.tools)
603
- if (attrs["ai.prompt.tools"] !== undefined) {
604
- delete attrs["ai.prompt.tools"];
605
- }
606
- }
607
- // ── Main processor ───────────────────────────────────────────────────────────
12
+ import { RespanLogType } from "@respan/respan-sdk";
13
+ import { VERCEL_PARENT_SPANS, VERCEL_SPAN_CONFIG } from "./constants/index.js";
14
+ import { formatCompletionOutput, formatPromptInput, formatToolInput, formatToolOutput, parseToolChoice, parseToolsValue } from "./_translator/messages.js";
15
+ import { AI_AGENT_ID, AI_MODEL_ID, AI_PREFIX, GEN_AI_REQUEST_MODEL, LLM_REQUEST_TYPE, RESPAN_LOG_TYPE, RESPAN_METADATA_AGENT_NAME, RESPAN_SPAN_TOOLS, TL_ENTITY_INPUT, TL_ENTITY_OUTPUT, TL_REQUEST_FUNCTIONS, TL_SPAN_KIND, isVercelAISpan, metadataKey, normalizeModel, resolveLogType, safeJsonStr, setDefault, } from "./_translator/shared.js";
16
+ import { enrichMetadata, enrichPerformanceMetrics, enrichTokens, stripRedundantAttrs } from "./_translator/span-enrichment.js";
608
17
  /**
609
18
  * SpanProcessor that translates Vercel AI SDK attributes to Traceloop/OpenLLMetry.
610
19
  *
@@ -614,99 +23,86 @@ function stripRedundantAttrs(attrs) {
614
23
  */
615
24
  export class VercelAITranslator {
616
25
  onStart(span, _parentContext) {
617
- // Cast to access attributes (Span interface doesn't expose them, but the impl does)
618
- const s = span;
619
- const name = s.name ?? "";
620
- if (!name.startsWith("ai."))
26
+ const writableSpan = span;
27
+ const name = writableSpan.name ?? "";
28
+ if (!name.startsWith(AI_PREFIX)) {
621
29
  return;
622
- // Set RESPAN_LOG_TYPE early so CompositeProcessor accepts this span
30
+ }
623
31
  const config = VERCEL_SPAN_CONFIG[name];
624
32
  if (config) {
625
- s.setAttribute(RESPAN_LOG_TYPE, config.logType);
626
- }
627
- else if (VERCEL_PARENT_SPANS[name] !== undefined) {
628
- s.setAttribute(RESPAN_LOG_TYPE, VERCEL_PARENT_SPANS[name]);
33
+ writableSpan.setAttribute(RESPAN_LOG_TYPE, config.logType);
34
+ return;
629
35
  }
630
- else {
631
- // Unknown ai.* span — use full fallback detection
632
- // At onStart, attributes may be sparse, so set a generic type.
633
- // The precise type will be resolved in onEnd() with full attributes.
634
- s.setAttribute(RESPAN_LOG_TYPE, RespanLogType.TASK);
36
+ const parentLogType = VERCEL_PARENT_SPANS[name];
37
+ if (parentLogType !== undefined) {
38
+ writableSpan.setAttribute(RESPAN_LOG_TYPE, parentLogType);
39
+ return;
635
40
  }
41
+ writableSpan.setAttribute(RESPAN_LOG_TYPE, RespanLogType.TASK);
636
42
  }
637
43
  onEnd(span) {
638
44
  const attrs = span.attributes;
639
- if (!attrs)
640
- return;
641
- if (!isVercelAISpan(span))
45
+ if (!attrs || !isVercelAISpan(span)) {
642
46
  return;
47
+ }
643
48
  const name = span.name;
644
49
  const config = VERCEL_SPAN_CONFIG[name];
645
50
  const parentLogType = VERCEL_PARENT_SPANS[name];
646
- // Resolve the log type using full fallback chain (name → operationId → attributes)
647
51
  const logType = resolveLogType(name, attrs);
648
- // ── Always: metadata, customer params, environment ────────────────────
649
- enrichMetadata(attrs, name);
650
- // ── Parent wrapper spans: minimal enrichment only ─────────────────────
52
+ enrichMetadata(attrs);
651
53
  if (parentLogType !== undefined && !config) {
652
54
  setDefault(attrs, RESPAN_LOG_TYPE, logType);
653
55
  stripRedundantAttrs(attrs);
654
56
  return;
655
57
  }
656
- // ── Detailed / leaf spans: full enrichment ────────────────────────────
657
- // Update RESPAN_LOG_TYPE with the resolved type (may be more accurate than onStart)
658
58
  attrs[RESPAN_LOG_TYPE] = logType;
659
59
  if (config) {
660
60
  setDefault(attrs, TL_SPAN_KIND, config.kind);
661
- // LLM-specific enrichment
662
61
  if (config.isLLM) {
663
62
  setDefault(attrs, LLM_REQUEST_TYPE, RespanLogType.CHAT);
664
- // Model
665
- const modelId = attrs["ai.model.id"];
63
+ const modelId = attrs[AI_MODEL_ID];
666
64
  if (modelId) {
667
65
  setDefault(attrs, GEN_AI_REQUEST_MODEL, normalizeModel(String(modelId)));
668
66
  }
669
- // Prompt messages → entity input
670
67
  const input = formatPromptInput(attrs);
671
- if (input)
68
+ if (input) {
672
69
  setDefault(attrs, TL_ENTITY_INPUT, input);
673
- // Completion → entity output
70
+ }
674
71
  const output = formatCompletionOutput(attrs);
675
- if (output)
72
+ if (output) {
676
73
  setDefault(attrs, TL_ENTITY_OUTPUT, output);
677
- // Token counts
74
+ }
678
75
  enrichTokens(attrs);
679
- // Tool definitions
680
- const tools = parseTools(attrs);
681
- if (tools)
682
- setDefault(attrs, RESPAN_SPAN_TOOLS, tools);
683
- // Tool choice
76
+ const toolsValue = parseToolsValue(attrs);
77
+ if (toolsValue) {
78
+ const tools = safeJsonStr(toolsValue);
79
+ attrs[RESPAN_SPAN_TOOLS] = tools;
80
+ attrs[TL_REQUEST_FUNCTIONS] = tools;
81
+ }
684
82
  const toolChoice = parseToolChoice(attrs);
685
- if (toolChoice)
83
+ if (toolChoice) {
686
84
  setDefault(attrs, metadataKey("tool_choice"), toolChoice);
687
- // Performance metrics (stream, TTFT, cost, etc.)
85
+ }
688
86
  enrichPerformanceMetrics(attrs, name);
689
87
  }
690
- // Tool call spans
691
88
  if (config.logType === RespanLogType.TOOL || logType === RespanLogType.TOOL) {
692
89
  const toolInput = formatToolInput(attrs);
693
- if (toolInput)
90
+ if (toolInput) {
694
91
  setDefault(attrs, TL_ENTITY_INPUT, toolInput);
92
+ }
695
93
  const toolOutput = formatToolOutput(attrs);
696
- if (toolOutput)
94
+ if (toolOutput) {
697
95
  setDefault(attrs, TL_ENTITY_OUTPUT, toolOutput);
96
+ }
698
97
  }
699
- // Agent spans
700
98
  if (config.logType === RespanLogType.AGENT || logType === RespanLogType.AGENT) {
701
- const agentName = attrs["ai.agent.name"] ?? attrs["ai.agent.id"] ?? name;
99
+ const agentName = attrs["ai.agent.name"] ?? attrs[AI_AGENT_ID] ?? name;
702
100
  setDefault(attrs, RESPAN_METADATA_AGENT_NAME, String(agentName));
703
101
  }
704
102
  }
705
103
  else {
706
- // Unknown ai.* span — enrich with fallback-resolved type
707
- // If fallback detected it as an LLM span, add model + tokens
708
104
  if (logType === RespanLogType.TEXT || logType === RespanLogType.EMBEDDING) {
709
- const modelId = attrs["ai.model.id"];
105
+ const modelId = attrs[AI_MODEL_ID];
710
106
  if (modelId) {
711
107
  setDefault(attrs, GEN_AI_REQUEST_MODEL, normalizeModel(String(modelId)));
712
108
  }
@@ -714,28 +110,34 @@ export class VercelAITranslator {
714
110
  if (logType === RespanLogType.TEXT) {
715
111
  setDefault(attrs, LLM_REQUEST_TYPE, RespanLogType.CHAT);
716
112
  const input = formatPromptInput(attrs);
717
- if (input)
113
+ if (input) {
718
114
  setDefault(attrs, TL_ENTITY_INPUT, input);
115
+ }
719
116
  const output = formatCompletionOutput(attrs);
720
- if (output)
117
+ if (output) {
721
118
  setDefault(attrs, TL_ENTITY_OUTPUT, output);
119
+ }
722
120
  enrichPerformanceMetrics(attrs, name);
723
121
  }
724
122
  }
725
- // If fallback detected tool, add tool input/output
726
123
  if (logType === RespanLogType.TOOL) {
727
124
  const toolInput = formatToolInput(attrs);
728
- if (toolInput)
125
+ if (toolInput) {
729
126
  setDefault(attrs, TL_ENTITY_INPUT, toolInput);
127
+ }
730
128
  const toolOutput = formatToolOutput(attrs);
731
- if (toolOutput)
129
+ if (toolOutput) {
732
130
  setDefault(attrs, TL_ENTITY_OUTPUT, toolOutput);
131
+ }
733
132
  }
734
133
  }
735
- // ── Cleanup: remove redundant Vercel attrs that have been translated ──
736
134
  stripRedundantAttrs(attrs);
737
135
  }
738
- async shutdown() { }
739
- async forceFlush() { }
136
+ forceFlush() {
137
+ return Promise.resolve();
138
+ }
139
+ shutdown() {
140
+ return Promise.resolve();
141
+ }
740
142
  }
741
143
  //# sourceMappingURL=_translator.js.map