UNPKG

langsmith

Version:

Client library to connect to the LangSmith Observability and Evaluation Platform.

1,256 lines (1,255 loc) 190 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = exports.AutoBatchQueue = exports.DEFAULT_MAX_SIZE_BYTES = exports.DEFAULT_UNCOMPRESSED_BATCH_SIZE_LIMIT_BYTES = void 0; exports.mergeRuntimeEnvIntoRun = mergeRuntimeEnvIntoRun; const uuid = __importStar(require("uuid")); const translator_js_1 = require("./experimental/otel/translator.cjs"); const otel_js_1 = require("./singletons/otel.cjs"); const async_caller_js_1 = require("./utils/async_caller.cjs"); const messages_js_1 = require("./utils/messages.cjs"); const env_js_1 = require("./utils/env.cjs"); const index_js_1 = require("./index.cjs"); const _uuid_js_1 = require("./utils/_uuid.cjs"); const warn_js_1 = require("./utils/warn.cjs"); const prompts_js_1 = require("./utils/prompts.cjs"); const error_js_1 = require("./utils/error.cjs"); const index_js_2 = require("./utils/prompt_cache/index.cjs"); const fsUtils = __importStar(require("./utils/fs.cjs")); const fetch_js_1 = require("./singletons/fetch.cjs"); const index_js_3 = require("./utils/fast-safe-stringify/index.cjs"); function mergeRuntimeEnvIntoRun(run, cachedEnvVars, omitTracedRuntimeInfo) { if (omitTracedRuntimeInfo) { return run; } const runtimeEnv = (0, env_js_1.getRuntimeEnvironment)(); const envVars = cachedEnvVars ?? (0, env_js_1.getLangSmithEnvVarsMetadata)(); const extra = run.extra ?? {}; const metadata = extra.metadata; run.extra = { ...extra, runtime: { ...runtimeEnv, ...extra?.runtime, }, metadata: { ...envVars, ...(envVars.revision_id || ("revision_id" in run && run.revision_id) ? { revision_id: ("revision_id" in run ? run.revision_id : undefined) ?? envVars.revision_id, } : {}), ...metadata, }, }; return run; } const getTracingSamplingRate = (configRate) => { const samplingRateStr = configRate?.toString() ?? (0, env_js_1.getLangSmithEnvironmentVariable)("TRACING_SAMPLING_RATE"); if (samplingRateStr === undefined) { return undefined; } const samplingRate = parseFloat(samplingRateStr); if (samplingRate < 0 || samplingRate > 1) { throw new Error(`LANGSMITH_TRACING_SAMPLING_RATE must be between 0 and 1 if set. Got: ${samplingRate}`); } return samplingRate; }; // utility functions const isLocalhost = (url) => { const strippedUrl = url.replace("http://", "").replace("https://", ""); const hostname = strippedUrl.split("/")[0].split(":")[0]; return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"); }; async function toArray(iterable) { const result = []; for await (const item of iterable) { result.push(item); } return result; } function trimQuotes(str) { if (str === undefined) { return undefined; } return str .trim() .replace(/^"(.*)"$/, "$1") .replace(/^'(.*)'$/, "$1"); } const handle429 = async (response) => { if (response?.status === 429) { const retryAfter = parseInt(response.headers.get("retry-after") ?? "10", 10) * 1000; if (retryAfter > 0) { await new Promise((resolve) => setTimeout(resolve, retryAfter)); // Return directly after calling this check return true; } } // Fall back to existing status checks return false; }; function _formatFeedbackScore(score) { if (typeof score === "number") { // Truncate at 4 decimal places return Number(score.toFixed(4)); } return score; } exports.DEFAULT_UNCOMPRESSED_BATCH_SIZE_LIMIT_BYTES = 24 * 1024 * 1024; /** Default maximum memory (1GB) for queue size limits. */ exports.DEFAULT_MAX_SIZE_BYTES = 1024 * 1024 * 1024; // 1GB const SERVER_INFO_REQUEST_TIMEOUT_MS = 10000; /** Maximum number of operations to batch in a single request. */ const DEFAULT_BATCH_SIZE_LIMIT = 100; const DEFAULT_API_URL = "https://api.smith.langchain.com"; class AutoBatchQueue { constructor(maxSizeBytes) { Object.defineProperty(this, "items", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "sizeBytes", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "maxSizeBytes", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.maxSizeBytes = maxSizeBytes ?? exports.DEFAULT_MAX_SIZE_BYTES; } peek() { return this.items[0]; } push(item) { let itemPromiseResolve; const itemPromise = new Promise((resolve) => { // Setting itemPromiseResolve is synchronous with promise creation: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise itemPromiseResolve = resolve; }); const size = (0, index_js_3.serialize)(item.item, `Serializing run with id: ${item.item.id}`).length; // Check if adding this item would exceed the size limit // Allow the run if the queue is empty (to support large single traces) if (this.sizeBytes + size > this.maxSizeBytes && this.items.length > 0) { console.warn(`AutoBatchQueue size limit (${this.maxSizeBytes} bytes) exceeded. Dropping run with id: ${item.item.id}. ` + `Current queue size: ${this.sizeBytes} bytes, attempted addition: ${size} bytes.`); // Resolve immediately to avoid blocking caller itemPromiseResolve(); return itemPromise; } this.items.push({ action: item.action, payload: item.item, otelContext: item.otelContext, apiKey: item.apiKey, apiUrl: item.apiUrl, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion itemPromiseResolve: itemPromiseResolve, itemPromise, size, }); this.sizeBytes += size; return itemPromise; } pop({ upToSizeBytes, upToSize, }) { if (upToSizeBytes < 1) { throw new Error("Number of bytes to pop off may not be less than 1."); } const popped = []; let poppedSizeBytes = 0; // Pop items until we reach or exceed the size limit while (poppedSizeBytes + (this.peek()?.size ?? 0) < upToSizeBytes && this.items.length > 0 && popped.length < upToSize) { const item = this.items.shift(); if (item) { popped.push(item); poppedSizeBytes += item.size; this.sizeBytes -= item.size; } } // If there is an item on the queue we were unable to pop, // just return it as a single batch. if (popped.length === 0 && this.items.length > 0) { const item = this.items.shift(); popped.push(item); poppedSizeBytes += item.size; this.sizeBytes -= item.size; } return [ popped.map((it) => ({ action: it.action, item: it.payload, otelContext: it.otelContext, apiKey: it.apiKey, apiUrl: it.apiUrl, size: it.size, })), () => popped.forEach((it) => it.itemPromiseResolve()), ]; } } exports.AutoBatchQueue = AutoBatchQueue; class Client { get _fetch() { return this.fetchImplementation || (0, fetch_js_1._getFetchImplementation)(this.debug); } constructor(config = {}) { Object.defineProperty(this, "apiKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "apiUrl", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "webUrl", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "workspaceId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "caller", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "batchIngestCaller", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "timeout_ms", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_tenantId", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "hideInputs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "hideOutputs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "omitTracedRuntimeInfo", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "tracingSampleRate", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "filteredPostUuids", { enumerable: true, configurable: true, writable: true, value: new Set() }); Object.defineProperty(this, "autoBatchTracing", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "autoBatchQueue", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "autoBatchTimeout", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "autoBatchAggregationDelayMs", { enumerable: true, configurable: true, writable: true, value: 250 }); Object.defineProperty(this, "batchSizeBytesLimit", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "batchSizeLimit", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "fetchOptions", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "settings", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "blockOnRootRunFinalization", { enumerable: true, configurable: true, writable: true, value: (0, env_js_1.getEnvironmentVariable)("LANGSMITH_TRACING_BACKGROUND") === "false" }); Object.defineProperty(this, "traceBatchConcurrency", { enumerable: true, configurable: true, writable: true, value: 5 }); Object.defineProperty(this, "_serverInfo", { enumerable: true, configurable: true, writable: true, value: void 0 }); // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.defineProperty(this, "_getServerInfoPromise", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "manualFlushMode", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "langSmithToOTELTranslator", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "fetchImplementation", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cachedLSEnvVarsForMetadata", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_promptCache", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "multipartStreamingDisabled", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "_multipartDisabled", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "_runCompressionDisabled", { enumerable: true, configurable: true, writable: true, value: (0, env_js_1.getLangSmithEnvironmentVariable)("DISABLE_RUN_COMPRESSION") === "true" }); Object.defineProperty(this, "failedTracesDir", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "failedTracesMaxBytes", { enumerable: true, configurable: true, writable: true, value: 100 * 1024 * 1024 }); Object.defineProperty(this, "debug", { enumerable: true, configurable: true, writable: true, value: (0, env_js_1.getEnvironmentVariable)("LANGSMITH_DEBUG") === "true" }); const defaultConfig = Client.getDefaultClientConfig(); this.tracingSampleRate = getTracingSamplingRate(config.tracingSamplingRate); this.apiUrl = trimQuotes(config.apiUrl ?? defaultConfig.apiUrl) ?? ""; if (this.apiUrl.endsWith("/")) { this.apiUrl = this.apiUrl.slice(0, -1); } this.apiKey = trimQuotes(config.apiKey ?? defaultConfig.apiKey); this.webUrl = trimQuotes(config.webUrl ?? defaultConfig.webUrl); if (this.webUrl?.endsWith("/")) { this.webUrl = this.webUrl.slice(0, -1); } this.workspaceId = trimQuotes(config.workspaceId ?? (0, env_js_1.getLangSmithEnvironmentVariable)("WORKSPACE_ID")); this.timeout_ms = config.timeout_ms ?? 90_000; this.caller = new async_caller_js_1.AsyncCaller({ ...(config.callerOptions ?? {}), maxRetries: 4, debug: config.debug ?? this.debug, }); this.traceBatchConcurrency = config.traceBatchConcurrency ?? this.traceBatchConcurrency; if (this.traceBatchConcurrency < 1) { throw new Error("Trace batch concurrency must be positive."); } this.debug = config.debug ?? this.debug; this.fetchImplementation = config.fetchImplementation; // Failed trace dump configuration this.failedTracesDir = (0, env_js_1.getLangSmithEnvironmentVariable)("FAILED_TRACES_DIR") || undefined; const failedTracesMb = (0, env_js_1.getLangSmithEnvironmentVariable)("FAILED_TRACES_MAX_MB"); if (failedTracesMb) { const n = parseInt(failedTracesMb, 10); if (Number.isFinite(n) && n > 0) { this.failedTracesMaxBytes = n * 1024 * 1024; } } // Use maxIngestMemoryBytes for both queues const maxMemory = config.maxIngestMemoryBytes ?? exports.DEFAULT_MAX_SIZE_BYTES; this.batchIngestCaller = new async_caller_js_1.AsyncCaller({ maxRetries: 4, maxConcurrency: this.traceBatchConcurrency, maxQueueSizeBytes: maxMemory, ...(config.callerOptions ?? {}), onFailedResponseHook: handle429, debug: config.debug ?? this.debug, }); this.hideInputs = config.hideInputs ?? config.anonymizer ?? defaultConfig.hideInputs; this.hideOutputs = config.hideOutputs ?? config.anonymizer ?? defaultConfig.hideOutputs; this.omitTracedRuntimeInfo = config.omitTracedRuntimeInfo ?? false; this.autoBatchTracing = config.autoBatchTracing ?? this.autoBatchTracing; this.autoBatchQueue = new AutoBatchQueue(maxMemory); this.blockOnRootRunFinalization = config.blockOnRootRunFinalization ?? this.blockOnRootRunFinalization; this.batchSizeBytesLimit = config.batchSizeBytesLimit; this.batchSizeLimit = config.batchSizeLimit; this.fetchOptions = config.fetchOptions || {}; this.manualFlushMode = config.manualFlushMode ?? this.manualFlushMode; if ((0, env_js_1.getOtelEnabled)()) { this.langSmithToOTELTranslator = new translator_js_1.LangSmithToOTELTranslator(); } // Cache metadata env vars once during construction to avoid repeatedly scanning process.env this.cachedLSEnvVarsForMetadata = (0, env_js_1.getLangSmithEnvVarsMetadata)(); // Initialize prompt cache // Handle backwards compatibility for deprecated `cache` parameter if (config.cache !== undefined && config.disablePromptCache) { (0, warn_js_1.warnOnce)("Both 'cache' and 'disablePromptCache' were provided. " + "The 'cache' parameter is deprecated and will be removed in a future version. " + "Using 'cache' parameter value."); } if (config.cache !== undefined) { (0, warn_js_1.warnOnce)("The 'cache' parameter is deprecated and will be removed in a future version. " + "Use 'configureGlobalPromptCache()' to configure the global cache, or " + "'disablePromptCache: true' to disable caching for this client."); // Handle old cache parameter if (config.cache === false) { this._promptCache = undefined; } else if (config.cache === true) { this._promptCache = index_js_2.promptCacheSingleton; } else { // Custom PromptCache instance provided this._promptCache = config.cache; } } else if (!config.disablePromptCache) { // Use the global singleton instance this._promptCache = index_js_2.promptCacheSingleton; } } static getDefaultClientConfig() { const apiKey = (0, env_js_1.getLangSmithEnvironmentVariable)("API_KEY"); const apiUrl = (0, env_js_1.getLangSmithEnvironmentVariable)("ENDPOINT") ?? DEFAULT_API_URL; const hideInputs = (0, env_js_1.getLangSmithEnvironmentVariable)("HIDE_INPUTS") === "true"; const hideOutputs = (0, env_js_1.getLangSmithEnvironmentVariable)("HIDE_OUTPUTS") === "true"; return { apiUrl: apiUrl, apiKey: apiKey, webUrl: undefined, hideInputs: hideInputs, hideOutputs: hideOutputs, }; } getHostUrl() { if (this.webUrl) { return this.webUrl; } else if (isLocalhost(this.apiUrl)) { this.webUrl = "http://localhost:3000"; return this.webUrl; } else if (this.apiUrl.endsWith("/api/v1")) { this.webUrl = this.apiUrl.replace("/api/v1", ""); return this.webUrl; } else if (this.apiUrl.includes("/api") && !this.apiUrl.split(".", 1)[0].endsWith("api")) { this.webUrl = this.apiUrl.replace("/api", ""); return this.webUrl; } else if (this.apiUrl.split(".", 1)[0].includes("dev")) { this.webUrl = "https://dev.smith.langchain.com"; return this.webUrl; } else if (this.apiUrl.split(".", 1)[0].includes("eu")) { this.webUrl = "https://eu.smith.langchain.com"; return this.webUrl; } else if (this.apiUrl.split(".", 1)[0].includes("beta")) { this.webUrl = "https://beta.smith.langchain.com"; return this.webUrl; } else { this.webUrl = "https://smith.langchain.com"; return this.webUrl; } } get headers() { const headers = { "User-Agent": `langsmith-js/${index_js_1.__version__}`, }; if (this.apiKey) { headers["x-api-key"] = `${this.apiKey}`; } if (this.workspaceId) { headers["x-tenant-id"] = this.workspaceId; } return headers; } _getPlatformEndpointPath(path) { // Check if apiUrl already ends with /v1 or /v1/ to avoid double /v1/v1/ paths const needsV1Prefix = this.apiUrl.slice(-3) !== "/v1" && this.apiUrl.slice(-4) !== "/v1/"; return needsV1Prefix ? `/v1/platform/${path}` : `/platform/${path}`; } async processInputs(inputs) { if (this.hideInputs === false) { return inputs; } if (this.hideInputs === true) { return {}; } if (typeof this.hideInputs === "function") { return this.hideInputs(inputs); } return inputs; } async processOutputs(outputs) { if (this.hideOutputs === false) { return outputs; } if (this.hideOutputs === true) { return {}; } if (typeof this.hideOutputs === "function") { return this.hideOutputs(outputs); } return outputs; } async prepareRunCreateOrUpdateInputs(run) { const runParams = { ...run }; if (runParams.inputs !== undefined) { runParams.inputs = await this.processInputs(runParams.inputs); } if (runParams.outputs !== undefined) { runParams.outputs = await this.processOutputs(runParams.outputs); } return runParams; } async _getResponse(path, queryParams) { const paramsString = queryParams?.toString() ?? ""; const url = `${this.apiUrl}${path}?${paramsString}`; const response = await this.caller.call(async () => { const res = await this._fetch(url, { method: "GET", headers: this.headers, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(res, `fetch ${path}`); return res; }); return response; } async _get(path, queryParams) { const response = await this._getResponse(path, queryParams); return response.json(); } async *_getPaginated(path, queryParams = new URLSearchParams(), transform) { let offset = Number(queryParams.get("offset")) || 0; const limit = Number(queryParams.get("limit")) || 100; while (true) { queryParams.set("offset", String(offset)); queryParams.set("limit", String(limit)); const url = `${this.apiUrl}${path}?${queryParams}`; const response = await this.caller.call(async () => { const res = await this._fetch(url, { method: "GET", headers: this.headers, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(res, `fetch ${path}`); return res; }); const items = transform ? transform(await response.json()) : await response.json(); if (items.length === 0) { break; } yield items; if (items.length < limit) { break; } offset += items.length; } } async *_getCursorPaginatedList(path, body = null, requestMethod = "POST", dataKey = "runs") { const bodyParams = body ? { ...body } : {}; while (true) { const body = JSON.stringify(bodyParams); const response = await this.caller.call(async () => { const res = await this._fetch(`${this.apiUrl}${path}`, { method: requestMethod, headers: { ...this.headers, "Content-Type": "application/json" }, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, body, }); await (0, error_js_1.raiseForStatus)(res, `fetch ${path}`); return res; }); const responseBody = await response.json(); if (!responseBody) { break; } if (!responseBody[dataKey]) { break; } yield responseBody[dataKey]; const cursors = responseBody.cursors; if (!cursors) { break; } if (!cursors.next) { break; } bodyParams.cursor = cursors.next; } } // Allows mocking for tests _shouldSample() { if (this.tracingSampleRate === undefined) { return true; } return Math.random() < this.tracingSampleRate; } _filterForSampling(runs, patch = false) { if (this.tracingSampleRate === undefined) { return runs; } if (patch) { const sampled = []; for (const run of runs) { if (!this.filteredPostUuids.has(run.trace_id)) { sampled.push(run); } else if (run.id === run.trace_id) { this.filteredPostUuids.delete(run.trace_id); } } return sampled; } else { // For new runs, sample at trace level to maintain consistency const sampled = []; for (const run of runs) { const traceId = run.trace_id ?? run.id; // If we've already made a decision about this trace, follow it if (this.filteredPostUuids.has(traceId)) { continue; } // For new traces, apply sampling if (run.id === traceId) { if (this._shouldSample()) { sampled.push(run); } else { this.filteredPostUuids.add(traceId); } } else { // Child runs follow their trace's sampling decision sampled.push(run); } } return sampled; } } async _getBatchSizeLimitBytes() { const serverInfo = await this._ensureServerInfo(); return (this.batchSizeBytesLimit ?? serverInfo?.batch_ingest_config?.size_limit_bytes ?? exports.DEFAULT_UNCOMPRESSED_BATCH_SIZE_LIMIT_BYTES); } /** * Get the maximum number of operations to batch in a single request. */ async _getBatchSizeLimit() { const serverInfo = await this._ensureServerInfo(); return (this.batchSizeLimit ?? serverInfo?.batch_ingest_config?.size_limit ?? DEFAULT_BATCH_SIZE_LIMIT); } async _getDatasetExamplesMultiPartSupport() { const serverInfo = await this._ensureServerInfo(); return (serverInfo.instance_flags?.dataset_examples_multipart_enabled ?? false); } drainAutoBatchQueue({ batchSizeLimitBytes, batchSizeLimit, }) { const promises = []; while (this.autoBatchQueue.items.length > 0) { const [batch, done] = this.autoBatchQueue.pop({ upToSizeBytes: batchSizeLimitBytes, upToSize: batchSizeLimit, }); if (!batch.length) { done(); break; } const batchesByDestination = batch.reduce((acc, item) => { const apiUrl = item.apiUrl ?? this.apiUrl; const apiKey = item.apiKey ?? this.apiKey; const isDefault = item.apiKey === this.apiKey && item.apiUrl === this.apiUrl; const batchKey = isDefault ? "default" : `${apiUrl}|${apiKey}`; if (!acc[batchKey]) { acc[batchKey] = []; } acc[batchKey].push(item); return acc; }, {}); const batchPromises = []; for (const [batchKey, batch] of Object.entries(batchesByDestination)) { const batchPromise = this._processBatch(batch, { apiUrl: batchKey === "default" ? undefined : batchKey.split("|")[0], apiKey: batchKey === "default" ? undefined : batchKey.split("|")[1], }); batchPromises.push(batchPromise); } // Wait for all batches to complete, then call the overall done callback const allBatchesPromise = Promise.all(batchPromises).finally(done); promises.push(allBatchesPromise); } return Promise.all(promises); } /** * Persist a failed trace payload to a local fallback directory. * * Saves a self-contained JSON file containing the endpoint path, the HTTP * headers required for replay, and the base64-encoded request body. * Can be replayed later with a simple POST: * * POST /<endpoint> * Content-Type: <value from saved headers> * [Content-Encoding: <value from saved headers>] * <decoded body> */ static async _writeTraceToFallbackDir(directory, body, replayHeaders, endpoint, maxBytes) { try { const bodyBuffer = typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body); const envelope = JSON.stringify({ version: 1, endpoint, headers: replayHeaders, body_base64: bodyBuffer.toString("base64"), }); const filename = `trace_${Date.now()}_${uuid.v4().slice(0, 8)}.json`; const filepath = fsUtils.path.join(directory, filename); if (!Client._fallbackDirsCreated.has(directory)) { await fsUtils.mkdir(directory); Client._fallbackDirsCreated.add(directory); } // Check budget before writing — drop new traces if over limit. if (maxBytes !== undefined && maxBytes > 0) { try { const entries = await fsUtils.readdir(directory); const traceFiles = entries.filter((f) => f.startsWith("trace_") && f.endsWith(".json")); let total = 0; for (const name of traceFiles) { const { size } = await fsUtils.stat(fsUtils.path.join(directory, name)); total += size; } if (total >= maxBytes) { console.warn(`Could not write trace to fallback dir ${directory} as it's ` + `already over size limit (${total} bytes >= ${maxBytes} bytes). ` + `Increase LANGSMITH_FAILED_TRACES_MAX_MB if possible.`); return; } } catch { // budget check errors must never prevent writing } } await fsUtils.writeFileAtomic(filepath, envelope); console.warn(`LangSmith trace upload failed; data saved to ${filepath} for later replay.`); } catch (writeErr) { console.error(`LangSmith tracing error: could not write trace to fallback dir ${directory}:`, writeErr); } } async _processBatch(batch, options) { if (!batch.length) { return; } // Calculate total batch size for queue tracking const batchSizeBytes = batch.reduce((sum, item) => sum + (item.size ?? 0), 0); try { if (this.langSmithToOTELTranslator !== undefined) { this._sendBatchToOTELTranslator(batch); } else { const ingestParams = { runCreates: batch .filter((item) => item.action === "create") .map((item) => item.item), runUpdates: batch .filter((item) => item.action === "update") .map((item) => item.item), }; const serverInfo = await this._ensureServerInfo(); const useMultipart = !this._multipartDisabled && (serverInfo?.batch_ingest_config?.use_multipart_endpoint ?? true); if (useMultipart) { const useGzip = !this._runCompressionDisabled && serverInfo?.instance_flags?.gzip_body_enabled; try { await this.multipartIngestRuns(ingestParams, { ...options, useGzip, sizeBytes: batchSizeBytes, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { if ((0, error_js_1.isLangSmithNotFoundError)(e)) { // Fallback to batch ingest if multipart endpoint returns 404 // Disable multipart for future requests this._multipartDisabled = true; await this.batchIngestRuns(ingestParams, { ...options, sizeBytes: batchSizeBytes, }); } else { throw e; } } } else { await this.batchIngestRuns(ingestParams, { ...options, sizeBytes: batchSizeBytes, }); } } } catch (e) { console.error("Error exporting batch:", e); } } _sendBatchToOTELTranslator(batch) { if (this.langSmithToOTELTranslator !== undefined) { const otelContextMap = new Map(); const operations = []; for (const item of batch) { if (item.item.id && item.otelContext) { otelContextMap.set(item.item.id, item.otelContext); if (item.action === "create") { operations.push({ operation: "post", id: item.item.id, trace_id: item.item.trace_id ?? item.item.id, run: item.item, }); } else { operations.push({ operation: "patch", id: item.item.id, trace_id: item.item.trace_id ?? item.item.id, run: item.item, }); } } } this.langSmithToOTELTranslator.exportBatch(operations, otelContextMap); } } async processRunOperation(item) { clearTimeout(this.autoBatchTimeout); this.autoBatchTimeout = undefined; item.item = mergeRuntimeEnvIntoRun(item.item, this.cachedLSEnvVarsForMetadata, this.omitTracedRuntimeInfo); const itemPromise = this.autoBatchQueue.push(item); if (this.manualFlushMode) { // Rely on manual flushing in serverless environments return itemPromise; } const sizeLimitBytes = await this._getBatchSizeLimitBytes(); const sizeLimit = await this._getBatchSizeLimit(); if (this.autoBatchQueue.sizeBytes > sizeLimitBytes || this.autoBatchQueue.items.length > sizeLimit) { void this.drainAutoBatchQueue({ batchSizeLimitBytes: sizeLimitBytes, batchSizeLimit: sizeLimit, }); } if (this.autoBatchQueue.items.length > 0) { this.autoBatchTimeout = setTimeout(() => { this.autoBatchTimeout = undefined; void this.drainAutoBatchQueue({ batchSizeLimitBytes: sizeLimitBytes, batchSizeLimit: sizeLimit, }); }, this.autoBatchAggregationDelayMs); } return itemPromise; } async _getServerInfo() { const response = await this.caller.call(async () => { const res = await this._fetch(`${this.apiUrl}/info`, { method: "GET", headers: { Accept: "application/json" }, signal: AbortSignal.timeout(SERVER_INFO_REQUEST_TIMEOUT_MS), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(res, "get server info"); return res; }); const json = await response.json(); if (this.debug) { console.log("\n=== LangSmith Server Configuration ===\n" + JSON.stringify(json, null, 2) + "\n"); } return json; } async _ensureServerInfo() { if (this._getServerInfoPromise === undefined) { this._getServerInfoPromise = (async () => { if (this._serverInfo === undefined) { try { this._serverInfo = await this._getServerInfo(); } catch (e) { console.warn(`[LANGSMITH]: Failed to fetch info on supported operations. Falling back to batch operations and default limits. Info: ${e.status ?? "Unspecified status code"} ${e.message}`); } } return this._serverInfo ?? {}; })(); } return this._getServerInfoPromise.then((serverInfo) => { if (this._serverInfo === undefined) { this._getServerInfoPromise = undefined; } return serverInfo; }); } async _getSettings() { if (!this.settings) { this.settings = this._get("/settings"); } return await this.settings; } /** * Flushes current queued traces. */ async flush() { const sizeLimitBytes = await this._getBatchSizeLimitBytes(); const sizeLimit = await this._getBatchSizeLimit(); await this.drainAutoBatchQueue({ batchSizeLimitBytes: sizeLimitBytes, batchSizeLimit: sizeLimit, }); } _cloneCurrentOTELContext() { const otel_trace = (0, otel_js_1.getOTELTrace)(); const otel_context = (0, otel_js_1.getOTELContext)(); if (this.langSmithToOTELTranslator !== undefined) { const currentSpan = otel_trace.getActiveSpan(); if (currentSpan) { return otel_trace.setSpan(otel_context.active(), currentSpan); } } return undefined; } async createRun(run, options) { if (!this._filterForSampling([run]).length) { return; } const headers = { ...this.headers, "Content-Type": "application/json", }; const session_name = run.project_name; delete run.project_name; const runCreate = await this.prepareRunCreateOrUpdateInputs({ session_name, ...run, start_time: run.start_time ?? Date.now(), }); if (this.autoBatchTracing && runCreate.trace_id !== undefined && runCreate.dotted_order !== undefined) { const otelContext = this._cloneCurrentOTELContext(); void this.processRunOperation({ action: "create", item: runCreate, otelContext, apiKey: options?.apiKey, apiUrl: options?.apiUrl, }).catch(console.error); return; } const mergedRunCreateParam = mergeRuntimeEnvIntoRun(runCreate, this.cachedLSEnvVarsForMetadata, this.omitTracedRuntimeInfo); if (options?.apiKey !== undefined) { headers["x-api-key"] = options.apiKey; } if (options?.workspaceId !== undefined) { headers["x-tenant-id"] = options.workspaceId; } const body = (0, index_js_3.serialize)(mergedRunCreateParam, `Creating run with id: ${mergedRunCreateParam.id}`); await this.caller.call(async () => { const res = await this._fetch(`${options?.apiUrl ?? this.apiUrl}/runs`, { method: "POST", headers, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, body, }); await (0, error_js_1.raiseForStatus)(res, "create run", true); return res; }); } /** * Batch ingest/upsert multiple runs in the Langsmith system. * @param runs */ async batchIngestRuns({ runCreates, runUpdates, }, options) { if (runCreates === undefined && runUpdates === undefined) { return; } let preparedCreateParams = await Promise.all(runCreates?.map((create) => this.prepareRunCreateOrUpdateInputs(create)) ?? []); let preparedUpdateParams = await Promise.all(runUpdates?.map((update) => this.prepareRunCreateOrUpdateInputs(update)) ?? []); if (preparedCreateParams.length > 0 && preparedUpdateParams.length > 0) { const createById = preparedCreateParams.reduce((params, run) => { if (!run.id) { return params; } params[run.id] = run; return params; }, {}); const standaloneUpdates = []; for (const updateParam of preparedUpdateParams) { if (updateParam.id !== undefined && createById[updateParam.id]) { createById[updateParam.id] = { ...createById[updateParam.id], ...updateParam, }; } else { standaloneUpdates.push(updateParam); } } preparedCreateParams = Object.values(createById); preparedUpdateParams = standaloneUpdates; } const rawBatch = { post: preparedCreateParams, patch: preparedUpdateParams, }; if (!rawBatch.post.length && !rawBatch.patch.length) { return; } const batchChunks = { post: [], patch: [], }; for (const k of ["post", "patch"]) { const key = k; const batchItems = rawBatch[key].reverse(); let batchItem = batchItems.pop(); while (batchItem !== undefined) { // Type is wrong but this is a deprecated code path anyway batchChunks[key].push(batchItem); batchItem = batchItems.pop(); } } if (batchChunks.post.length > 0 || batchChunks.patch.length > 0) { const runIds = batchChunks.post .map((item) => item.id) .concat(batchChunks.patch.map((item) => item.id)) .join(","); await this._postBatchIngestRuns((0, index_js_3.serialize)(batchChunks, `Ingesting runs with ids: ${runIds}`), options); } } async _postBatchIngestRuns(body, options) { const headers = { ...this.headers, "Content-Type": "application/json", Accept: "application/json", }; if (options?.apiKey !== undefined) { headers["x-api-key"] = options.apiKey; } await this.batchIngestCaller.callWithOptions({ sizeBytes: options?.sizeBytes }, async () => { const res = await this._fetch(`${options?.apiUrl ?? this.apiUrl}/runs/batch`, { method: "POST", headers, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, body, }); await (0, error_js_1.raiseForStatus)(res, "batch create run", true); return res; }); } /** * Batch ingest/upsert multiple runs in the Langsmith system. * @param runs */ async multipartIngestRuns({ runCreates, runUpdates, }, options) { if (runCreates === undefined && runUpdates === undefined) { return; } // transform and convert to dicts const allAttachments = {}; let preparedCreateParams = []; for (const create of runCreates ?? []) { const preparedCreate = await this.prepareRunCreateOrUpdateInputs(create); if (preparedCreate.id !== undefined && preparedCreate.attachments !== undefined) { allAttachments[preparedCreate.id] = preparedCreate.attachments; } delete preparedCreate.attachments; preparedCreateParams.push(preparedCreate); } let preparedUpdateParams = []; for (const update of runUpdates ?? []) { preparedUpdateParams.push(await this.prepareRunCreateOrUpdateInputs(update)); } // require trace_id and dotted_order const invalidRunCreate = preparedCreateParams.find((runCreate) => { return (runCreate.trace_id === undefined || runCreate.dotted_order === undefined); }); if (invalidRunCreate !== undefined) { throw new Error(`Multipart ingest requires "trace_id" and "dotted_order" to be set when creating a run`); } const invalidRunUpdate = preparedUpdateParams.find((runUpdate) => { return (runUpdate.trace_id === undefined || runUpdate.dotted_order === undefined); }); if (invalidRunUpdate !== undefined) { throw new Error(`Multipart ingest requires "trace_id" and "dotted_order" to be set when updating a run`); } // combine post and patch dicts where possible if (preparedCreateParams.length > 0 && preparedUpdateParams.length > 0) { const createById = preparedCreateParams.reduce((params, run) => { if (!run.id) { return params; } params[run.id] = run; return params; }, {}); const standaloneUpdates = []; for (const updateParam of preparedUpdateParams) { if (updateParam.id !== undefined && createById[updateParam.id]) { createById[updateParam.id] = { ...createById[updateParam.id],