@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.
- 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 +181 -0
- package/dist/_translator/span-enrichment.js.map +1 -0
- package/dist/_translator.d.ts +2 -15
- package/dist/_translator.js +52 -650
- package/dist/_translator.js.map +1 -1
- package/package.json +2 -2
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, 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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
70
|
+
}
|
|
674
71
|
const output = formatCompletionOutput(attrs);
|
|
675
|
-
if (output)
|
|
72
|
+
if (output) {
|
|
676
73
|
setDefault(attrs, TL_ENTITY_OUTPUT, output);
|
|
677
|
-
|
|
74
|
+
}
|
|
678
75
|
enrichTokens(attrs);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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[
|
|
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[
|
|
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
|
-
|
|
739
|
-
|
|
136
|
+
forceFlush() {
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
shutdown() {
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
}
|
|
740
142
|
}
|
|
741
143
|
//# sourceMappingURL=_translator.js.map
|