langsmith
Version:
Client library to connect to the LangSmith Observability and Evaluation Platform.
173 lines (172 loc) • 6.86 kB
JavaScript
// Relaxed UUID validation regex (allows any valid UUID format including nil UUIDs)
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
import { v7 as uuidv7 } from "uuid";
import { warnOnce } from "./warn.js";
import { XXH3_128, xxh128ToBytes } from "./xxhash/xxhash.js";
let UUID7_WARNING_EMITTED = false;
export function assertUuid(str, which) {
// Use relaxed regex validation instead of strict uuid.validate()
// This allows edge cases like nil UUIDs or test UUIDs that might not pass strict validation
if (!UUID_REGEX.test(str)) {
const msg = which !== undefined
? `Invalid UUID for ${which}: ${str}`
: `Invalid UUID: ${str}`;
throw new Error(msg);
}
return str;
}
/**
* Generate a UUID v7 from a timestamp.
*
* @param timestamp - The timestamp in milliseconds
* @returns A UUID v7 string
*/
export function uuid7FromTime(timestamp) {
const msecs = typeof timestamp === "string" ? Date.parse(timestamp) : timestamp;
// Work around uuid@10 behavior where providing only { msecs }
// may not set the internal timestamp used for stringification.
// Providing a seq ensures the implementation updates its internal state
// and encodes the provided milliseconds into the UUID bytes.
return uuidv7({ msecs, seq: 0 });
}
/**
* Get the version of a UUID string.
* @param uuidStr - The UUID string to check
* @returns The version number (1-7) or null if invalid
*/
export function getUuidVersion(uuidStr) {
if (!UUID_REGEX.test(uuidStr)) {
return null;
}
// Version is in bits 48-51
// Format: xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx
const versionChar = uuidStr[14];
return parseInt(versionChar, 16);
}
/**
* Warn if a UUID is not version 7.
*
* @param uuidStr - The UUID string to check
* @param idType - The type of ID (e.g., "run_id", "trace_id") for the warning message
*/
export function warnIfNotUuidV7(uuidStr, _idType) {
const version = getUuidVersion(uuidStr);
if (version !== null && version !== 7 && !UUID7_WARNING_EMITTED) {
UUID7_WARNING_EMITTED = true;
warnOnce(`LangSmith now uses UUID v7 for run and trace identifiers. ` +
`This warning appears when passing custom IDs. ` +
`Please use: import { uuidv7 } from 'langsmith'; const id = uuidv7(); ` +
`Future versions will require UUID v7.`);
}
}
/**
* Convert a UUID string to its 16-byte representation.
* @param uuidStr - The UUID string (with or without dashes)
* @returns A Uint8Array containing the 16 bytes of the UUID
*/
function uuidToBytes(uuidStr) {
const hex = uuidStr.replace(/-/g, "");
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
/**
* Convert 16 bytes to a UUID string.
* @param bytes - A Uint8Array containing 16 bytes
* @returns A UUID string in standard format
*/
function bytesToUuid(bytes) {
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
// Reuse TextEncoder instance for performance
const _textEncoder = new TextEncoder();
/**
* Generates a 16-byte fingerprint for deterministic UUID generation using XXH3-128.
*
* XXH3 is an extremely fast, non-cryptographic hash function that provides excellent
* collision resistance. It's widely used in production systems and compatible with
* xxHash implementations in other languages.
*
* See: https://github.com/Cyan4973/xxHash
*
* @param str - The input string to hash
* @returns A Uint8Array containing 16 bytes of hash output
*/
function _fastHash128(str) {
const data = _textEncoder.encode(str);
// Compute XXH3-128 hash and convert to bytes
const hash128 = XXH3_128(data);
return xxh128ToBytes(hash128);
}
/**
* Generate a deterministic UUID v7 derived from an original UUID and a key.
*
* This function creates a new UUID that:
* - Preserves the timestamp from the original UUID if it's UUID v7
* - Uses current time if the original is not UUID v7
* - Uses deterministic "random" bits derived from hashing the original + key
* - Is valid UUID v7 format
*
* This is used for creating replica IDs that maintain time-ordering properties
* while being deterministic across distributed systems.
*
* @param originalId - The source UUID string (ideally UUID v7 to preserve timestamp)
* @param key - A string key used for deterministic derivation (e.g., project name)
* @returns A new UUID v7 string with preserved timestamp (if original is v7) and
* deterministic random bits
*
* @example
* ```typescript
* const original = uuidv7();
* const replicaId = nonCryptographicUuid7Deterministic(original, "replica-project");
* // Same inputs always produce same output
* assert(nonCryptographicUuid7Deterministic(original, "replica-project") === replicaId);
* ```
*/
export function nonCryptographicUuid7Deterministic(originalId, key) {
// Generate deterministic bytes from hash of original + key
const hashInput = `${originalId}:${key}`;
const h = _fastHash128(hashInput);
// Build new UUID7:
// UUID7 structure (RFC 9562):
// [0-5] 48 bits: unix_ts_ms (timestamp in milliseconds)
// [6] 4 bits: version (0111 = 7) + 4 bits rand_a
// [7] 8 bits: rand_a (continued)
// [8] 2 bits: variant (10) + 6 bits rand_b
// [9-15] 56 bits: rand_b (continued)
const b = new Uint8Array(16);
// Check if original is UUID v7 - if so, preserve its timestamp
// If not, use current time to ensure the derived UUID has a valid timestamp
const version = getUuidVersion(originalId);
if (version === 7) {
// Preserve timestamp from original UUID7 (bytes 0-5)
const originalBytes = uuidToBytes(originalId);
b.set(originalBytes.slice(0, 6), 0);
}
else {
// Generate fresh timestamp for non-UUID7 inputs
// This matches the uuid npm package's v7 implementation:
// https://github.com/uuidjs/uuid/blob/main/src/v7.ts
const msecs = Date.now();
b[0] = (msecs / 0x10000000000) & 0xff;
b[1] = (msecs / 0x100000000) & 0xff;
b[2] = (msecs / 0x1000000) & 0xff;
b[3] = (msecs / 0x10000) & 0xff;
b[4] = (msecs / 0x100) & 0xff;
b[5] = msecs & 0xff;
}
// Set version 7 (0111) in high nibble + 4 bits from hash
b[6] = 0x70 | (h[0] & 0x0f);
// rand_a continued (8 bits from hash)
b[7] = h[1];
// Set variant (10) in high 2 bits + 6 bits from hash
b[8] = 0x80 | (h[2] & 0x3f);
// rand_b (56 bits = 7 bytes from hash)
b.set(h.slice(3, 10), 9);
return bytesToUuid(b);
}