@respan/instrumentation-vercel 1.0.4 → 1.0.6
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/dist/_translator/messages.d.ts +7 -0
- package/dist/_translator/messages.js +439 -0
- package/dist/_translator/messages.js.map +1 -0
- package/dist/_translator/shared.d.ts +63 -0
- package/dist/_translator/shared.js +162 -0
- package/dist/_translator/shared.js.map +1 -0
- package/dist/_translator/span-enrichment.d.ts +5 -0
- package/dist/_translator/span-enrichment.js +188 -0
- package/dist/_translator/span-enrichment.js.map +1 -0
- package/dist/_translator.d.ts +2 -15
- package/dist/_translator.js +62 -651
- package/dist/_translator.js.map +1 -1
- package/package.json +1 -1
package/dist/_translator.js
CHANGED
|
@@ -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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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, 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,95 @@ function stripRedundantAttrs(attrs) {
|
|
|
614
23
|
*/
|
|
615
24
|
export class VercelAITranslator {
|
|
616
25
|
onStart(span, _parentContext) {
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
if (!name.startsWith("ai."))
|
|
26
|
+
const writableSpan = span;
|
|
27
|
+
const name = writableSpan.name ?? "";
|
|
28
|
+
if (!name.startsWith(AI_PREFIX)) {
|
|
621
29
|
return;
|
|
622
|
-
|
|
30
|
+
}
|
|
623
31
|
const config = VERCEL_SPAN_CONFIG[name];
|
|
624
32
|
if (config) {
|
|
625
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
-
//
|
|
60
|
+
// Do NOT set traceloop.span.kind for auto-emitted Vercel SDK spans.
|
|
61
|
+
// In the Respan composite processor `traceloop.span.kind` is reserved
|
|
62
|
+
// for user-decorated spans (withWorkflow / withTask / withAgent) and
|
|
63
|
+
// setting it on auto spans (a) flattens the parent/child tree and
|
|
64
|
+
// (b) causes LLM detail spans (doGenerate / doStream) to be classified
|
|
65
|
+
// as "task" instead of LLM in the backend. The respan.entity.log_type
|
|
66
|
+
// attribute (set above) carries the correct type for ingestion.
|
|
67
|
+
// Matches the patterns in respan-instrumentation-openinference (see
|
|
68
|
+
// _translator.ts:500) and respan-instrumentation-openai-agents
|
|
69
|
+
// (see _otel_emitter.ts:398).
|
|
662
70
|
if (config.isLLM) {
|
|
663
71
|
setDefault(attrs, LLM_REQUEST_TYPE, RespanLogType.CHAT);
|
|
664
|
-
|
|
665
|
-
const modelId = attrs["ai.model.id"];
|
|
72
|
+
const modelId = attrs[AI_MODEL_ID];
|
|
666
73
|
if (modelId) {
|
|
667
74
|
setDefault(attrs, GEN_AI_REQUEST_MODEL, normalizeModel(String(modelId)));
|
|
668
75
|
}
|
|
669
|
-
// Prompt messages → entity input
|
|
670
76
|
const input = formatPromptInput(attrs);
|
|
671
|
-
if (input)
|
|
77
|
+
if (input) {
|
|
672
78
|
setDefault(attrs, TL_ENTITY_INPUT, input);
|
|
673
|
-
|
|
79
|
+
}
|
|
674
80
|
const output = formatCompletionOutput(attrs);
|
|
675
|
-
if (output)
|
|
81
|
+
if (output) {
|
|
676
82
|
setDefault(attrs, TL_ENTITY_OUTPUT, output);
|
|
677
|
-
|
|
83
|
+
}
|
|
678
84
|
enrichTokens(attrs);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
85
|
+
const toolsValue = parseToolsValue(attrs);
|
|
86
|
+
if (toolsValue) {
|
|
87
|
+
const tools = safeJsonStr(toolsValue);
|
|
88
|
+
attrs[RESPAN_SPAN_TOOLS] = tools;
|
|
89
|
+
attrs[TL_REQUEST_FUNCTIONS] = tools;
|
|
90
|
+
}
|
|
684
91
|
const toolChoice = parseToolChoice(attrs);
|
|
685
|
-
if (toolChoice)
|
|
92
|
+
if (toolChoice) {
|
|
686
93
|
setDefault(attrs, metadataKey("tool_choice"), toolChoice);
|
|
687
|
-
|
|
94
|
+
}
|
|
688
95
|
enrichPerformanceMetrics(attrs, name);
|
|
689
96
|
}
|
|
690
|
-
// Tool call spans
|
|
691
97
|
if (config.logType === RespanLogType.TOOL || logType === RespanLogType.TOOL) {
|
|
692
98
|
const toolInput = formatToolInput(attrs);
|
|
693
|
-
if (toolInput)
|
|
99
|
+
if (toolInput) {
|
|
694
100
|
setDefault(attrs, TL_ENTITY_INPUT, toolInput);
|
|
101
|
+
}
|
|
695
102
|
const toolOutput = formatToolOutput(attrs);
|
|
696
|
-
if (toolOutput)
|
|
103
|
+
if (toolOutput) {
|
|
697
104
|
setDefault(attrs, TL_ENTITY_OUTPUT, toolOutput);
|
|
105
|
+
}
|
|
698
106
|
}
|
|
699
|
-
// Agent spans
|
|
700
107
|
if (config.logType === RespanLogType.AGENT || logType === RespanLogType.AGENT) {
|
|
701
|
-
const agentName = attrs["ai.agent.name"] ?? attrs[
|
|
108
|
+
const agentName = attrs["ai.agent.name"] ?? attrs[AI_AGENT_ID] ?? name;
|
|
702
109
|
setDefault(attrs, RESPAN_METADATA_AGENT_NAME, String(agentName));
|
|
703
110
|
}
|
|
704
111
|
}
|
|
705
112
|
else {
|
|
706
|
-
// Unknown ai.* span — enrich with fallback-resolved type
|
|
707
|
-
// If fallback detected it as an LLM span, add model + tokens
|
|
708
113
|
if (logType === RespanLogType.TEXT || logType === RespanLogType.EMBEDDING) {
|
|
709
|
-
const modelId = attrs[
|
|
114
|
+
const modelId = attrs[AI_MODEL_ID];
|
|
710
115
|
if (modelId) {
|
|
711
116
|
setDefault(attrs, GEN_AI_REQUEST_MODEL, normalizeModel(String(modelId)));
|
|
712
117
|
}
|
|
@@ -714,28 +119,34 @@ export class VercelAITranslator {
|
|
|
714
119
|
if (logType === RespanLogType.TEXT) {
|
|
715
120
|
setDefault(attrs, LLM_REQUEST_TYPE, RespanLogType.CHAT);
|
|
716
121
|
const input = formatPromptInput(attrs);
|
|
717
|
-
if (input)
|
|
122
|
+
if (input) {
|
|
718
123
|
setDefault(attrs, TL_ENTITY_INPUT, input);
|
|
124
|
+
}
|
|
719
125
|
const output = formatCompletionOutput(attrs);
|
|
720
|
-
if (output)
|
|
126
|
+
if (output) {
|
|
721
127
|
setDefault(attrs, TL_ENTITY_OUTPUT, output);
|
|
128
|
+
}
|
|
722
129
|
enrichPerformanceMetrics(attrs, name);
|
|
723
130
|
}
|
|
724
131
|
}
|
|
725
|
-
// If fallback detected tool, add tool input/output
|
|
726
132
|
if (logType === RespanLogType.TOOL) {
|
|
727
133
|
const toolInput = formatToolInput(attrs);
|
|
728
|
-
if (toolInput)
|
|
134
|
+
if (toolInput) {
|
|
729
135
|
setDefault(attrs, TL_ENTITY_INPUT, toolInput);
|
|
136
|
+
}
|
|
730
137
|
const toolOutput = formatToolOutput(attrs);
|
|
731
|
-
if (toolOutput)
|
|
138
|
+
if (toolOutput) {
|
|
732
139
|
setDefault(attrs, TL_ENTITY_OUTPUT, toolOutput);
|
|
140
|
+
}
|
|
733
141
|
}
|
|
734
142
|
}
|
|
735
|
-
// ── Cleanup: remove redundant Vercel attrs that have been translated ──
|
|
736
143
|
stripRedundantAttrs(attrs);
|
|
737
144
|
}
|
|
738
|
-
|
|
739
|
-
|
|
145
|
+
forceFlush() {
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
}
|
|
148
|
+
shutdown() {
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
740
151
|
}
|
|
741
152
|
//# sourceMappingURL=_translator.js.map
|