|
1 | 1 | 'use strict' |
2 | 2 |
|
| 3 | +const id = require('../id') |
3 | 4 | const log = require('../log') |
4 | 5 | const { |
5 | 6 | LLMOBS_PARENT_ID_BRIDGE_KEY, |
6 | 7 | LLMOBS_TRACE_ID_BRIDGE_KEY, |
7 | 8 | SPAN_KINDS, |
8 | 9 | } = require('./constants/tags') |
9 | 10 |
|
| 11 | +const DECIMAL_TRACE_ID_REGEX = /^\d+$/ |
| 12 | +const HEX_TRACE_ID_REGEX = /^[0-9a-f]{32}$/i |
| 13 | +const MAX_UINT_64 = (1n << 64n) - 1n |
| 14 | + |
10 | 15 | // LLM I/O is overwhelmingly ASCII (English prompts and code). Walk once |
11 | 16 | // looking for the first non-ASCII char; if there is none, hand the input |
12 | 17 | // straight back. Otherwise pick up the slow path from the byte that needed |
@@ -312,12 +317,12 @@ function safeJsonParse (value, fallback) { |
312 | 317 | // LLMObs root and hoists the gen_ai ancestors under it, inverting the trace. |
313 | 318 | /** |
314 | 319 | * @param {import('../opentracing/span')} span |
315 | | - * @param {{ includeParentId?: boolean }} [opts] |
| 320 | + * @param {{ includeParentId?: boolean, llmobsTraceId?: string }} [opts] |
316 | 321 | */ |
317 | | -function writeBridgeTags (span, { includeParentId = true } = {}) { |
| 322 | +function writeBridgeTags (span, { includeParentId = true, llmobsTraceId } = {}) { |
318 | 323 | const traceTags = span?.context?.()._trace?.tags |
319 | 324 | if (!traceTags || traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY]) return |
320 | | - traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = span.context().toTraceId(true) |
| 325 | + traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = llmobsTraceId ?? span.context().toTraceId(true) |
321 | 326 | if (includeParentId) { |
322 | 327 | traceTags[LLMOBS_PARENT_ID_BRIDGE_KEY] = span.context().toSpanId() |
323 | 328 | } |
@@ -362,6 +367,51 @@ function findGenAIAncestorSpanId (span) { |
362 | 367 | return null |
363 | 368 | } |
364 | 369 |
|
| 370 | +/** |
| 371 | + * Generate a 128-bit LLMObs trace ID with the span start time encoded in its high bits. |
| 372 | + * @param {number} startTime |
| 373 | + * @returns {string} |
| 374 | + */ |
| 375 | +function generateLlmObsTraceId (startTime) { |
| 376 | + const identifier = id() |
| 377 | + const traceIdHigh = Math.floor(startTime / 1000) |
| 378 | + .toString(16) |
| 379 | + .padStart(8, '0') |
| 380 | + .padEnd(16, '0') |
| 381 | + |
| 382 | + return identifier.toTraceIdHex(traceIdHigh).padStart(32, '0') |
| 383 | +} |
| 384 | + |
| 385 | +/** |
| 386 | + * Convert an internally stored hexadecimal LLMObs trace ID to its distributed wire representation. |
| 387 | + * @param {string | undefined} traceId |
| 388 | + * @returns {string | undefined} |
| 389 | + */ |
| 390 | +function llmObsTraceIdToWire (traceId) { |
| 391 | + if (!traceId) return |
| 392 | + if (!HEX_TRACE_ID_REGEX.test(traceId)) return traceId |
| 393 | + |
| 394 | + return BigInt(`0x${traceId}`).toString(10) |
| 395 | +} |
| 396 | + |
| 397 | +/** |
| 398 | + * Normalize a distributed LLMObs trace ID to the representation expected by LLMObs span events. |
| 399 | + * @param {string | undefined} traceId |
| 400 | + * @returns {string | undefined} |
| 401 | + */ |
| 402 | +function normalizeLlmObsTraceId (traceId) { |
| 403 | + if (!traceId) return |
| 404 | + |
| 405 | + if (HEX_TRACE_ID_REGEX.test(traceId) && (traceId[0] === '0' || !DECIMAL_TRACE_ID_REGEX.test(traceId))) { |
| 406 | + return traceId |
| 407 | + } |
| 408 | + |
| 409 | + if (!DECIMAL_TRACE_ID_REGEX.test(traceId)) return traceId |
| 410 | + |
| 411 | + const identifier = BigInt(traceId) |
| 412 | + return identifier > MAX_UINT_64 ? identifier.toString(16).padStart(32, '0') : traceId |
| 413 | +} |
| 414 | + |
365 | 415 | // Maps an audio `format` (e.g. "wav", "mp3") to a MIME type. Defaults to `audio/wav` when the |
366 | 416 | // format is missing. Provider-specific overrides (e.g. OpenAI's mp3 -> audio/mpeg) are passed in |
367 | 417 | // via `mimeTypeLookup` so this stays provider-agnostic. A non-string `format` is treated as missing |
@@ -396,6 +446,9 @@ module.exports = { |
396 | 446 | audioMimeTypeFromFormat, |
397 | 447 | encodeUnicode, |
398 | 448 | findGenAIAncestorSpanId, |
| 449 | + generateLlmObsTraceId, |
| 450 | + llmObsTraceIdToWire, |
| 451 | + normalizeLlmObsTraceId, |
399 | 452 | formatAudioPart, |
400 | 453 | validateCostTags, |
401 | 454 | validateKind, |
|
0 commit comments