langsmith
Version:
Client library to connect to the LangSmith Observability and Evaluation Platform.
1,256 lines (1,255 loc) • 190 kB
JavaScript
"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],