import * as uuid from "./utils/uuid/src/index.js";
import { LangSmithToOTELTranslator, } from "./experimental/otel/translator.js";
import { getDefaultOTLPTracerComponents, getOTELTrace, getOTELContext, } from "./singletons/otel.js";
import { AsyncCaller } from "./utils/async_caller.js";
import { convertLangChainMessageToExample, isLangChainMessage, } from "./utils/messages.js";
import { getEnvironmentVariable, getLangSmithEnvVarsMetadata, getLangSmithEnvironmentVariable, getRuntimeEnvironment, getEnv, resolveTracingMode, } from "./utils/env.js";
import { __version__ } from "./index.js";
import { assertUuid } from "./utils/_uuid.js";
import { warnOnce } from "./utils/warn.js";
import { parseHubIdentifier } from "./utils/prompts.js";
import { raiseForStatus, isLangSmithNotFoundError, isLangSmithConflictError, } from "./utils/error.js";
import { promptCacheSingleton, } from "./utils/prompt_cache/index.js";
import * as fsUtils from "./utils/fs.js";
import { _shouldStreamForGlobalFetchImplementation, _getFetchImplementation, } from "./singletons/fetch.js";
import { serialize as serializePayloadForTracing, estimateSerializedSize, } from "./utils/fast-safe-stringify/index.js";
import { getSharedSerializeWorker, hasLargeString, } from "./utils/serialize_worker.js";
/**
 * Catches timestamps without a timezone suffix.
 */
function _ensureUTCTimestamp(ts) {
    if (typeof ts === "string" &&
        ts.length > 0 &&
        !ts.includes("Z") &&
        !ts.includes("+") &&
        !ts.includes("-", 10)) {
        return ts + "Z";
    }
    return ts;
}
function _normalizeRunTimestamps(run) {
    return {
        ...run,
        start_time: _ensureUTCTimestamp(run.start_time),
        end_time: _ensureUTCTimestamp(run.end_time),
    };
}
export function mergeRuntimeEnvIntoRun(run, cachedEnvVars, omitTracedRuntimeInfo) {
    if (omitTracedRuntimeInfo) {
        return run;
    }
    const runtimeEnv = getRuntimeEnvironment();
    const envVars = cachedEnvVars ?? 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() ??
        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;
}
export const DEFAULT_UNCOMPRESSED_BATCH_SIZE_LIMIT_BYTES = 24 * 1024 * 1024;
/** Default maximum memory (1GB) for queue size limits. */
export const 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";
export 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 ?? 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;
        });
        // By default we compute the exact serialized size here by stringifying
        // the payload. This is expensive: JSON.stringify on large payloads
        // blocks the event loop on the user's hot path.
        //
        // Opting into LANGSMITH_PERF_OPTIMIZATION=true switches to a cheap
        // structural estimate instead. The estimate is only used for soft
        // memory accounting (queue size limit and downstream async caller
        // memory tracking), never for anything correctness-critical -- the
        // real serialization still happens later, off the hot path, when the
        // batch is assembled for sending.
        const size = getLangSmithEnvironmentVariable("PERF_OPTIMIZATION") === "true"
            ? estimateSerializedSize(item.item).size
            : serializePayloadForTracing(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()),
        ];
    }
}
export class Client {
    get tracingMode() {
        return this._tracingMode;
    }
    get _fetch() {
        return this.fetchImplementation || _getFetchImplementation(this.debug);
    }
    /**
     * Serialize a payload for tracing, optionally offloading the work to a
     * Node worker thread when LANGSMITH_PERF_OPTIMIZATION=true and the runtime
     * supports worker_threads.
     *
     * Falls back to synchronous serialization when:
     *  - the perf flag is off
     *  - manualFlushMode is enabled (serverless: worker boot cost > benefit)
     *  - worker_threads is unavailable (non-Node runtimes)
     *  - the payload contains values that can't be structured-cloned across
     *    threads (functions, non-cloneable class instances, streams, etc.)
     *  - the worker throws for any other reason
     *
     * In all fallback cases the returned bytes are identical to the sync path.
     */
    _trackDrain(promise) {
        this._pendingDrains.add(promise);
        promise.finally(() => {
            this._pendingDrains.delete(promise);
        });
    }
    async _serializeBody(payload, errorContext) {
        const perfOptIn = getLangSmithEnvironmentVariable("PERF_OPTIMIZATION") === "true";
        if (!perfOptIn || this.manualFlushMode) {
            return serializePayloadForTracing(payload, errorContext);
        }
        // Shape-aware gate: worker offload pays for itself only when the
        // payload is dominated by one or more large strings (V8 can refcount
        // those across isolates instead of copying). For structure-heavy
        // payloads -- many keys, deep nesting, lots of small strings -- the
        // structuredClone walk plus thread-hop cost exceeds the JSON.stringify
        // cost we would pay inline, so we fall through to sync serialize.
        if (!hasLargeString(payload)) {
            return serializePayloadForTracing(payload, errorContext);
        }
        if (this._serializeWorker === undefined) {
            this._serializeWorker = getSharedSerializeWorker();
        }
        if (this._serializeWorker === null) {
            return serializePayloadForTracing(payload, errorContext);
        }
        try {
            const bytes = await this._serializeWorker.serialize(payload);
            if (bytes === null) {
                // Worker subsystem unavailable; cache the null to skip re-entry.
                this._serializeWorker = null;
                return serializePayloadForTracing(payload, errorContext);
            }
            return bytes;
        }
        catch {
            // DataCloneError, worker crash, etc. Fall back silently.
            return serializePayloadForTracing(payload, errorContext);
        }
    }
    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, "hideMetadata", {
            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: 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, "_serializeWorker", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * Tracks in-flight drainAutoBatchQueue promises so awaitPendingTraceBatches
         * can wait on them even if the flush involves async work (worker-thread
         * serialize) that hasn't yet registered with batchIngestCaller.queue.
         */
        Object.defineProperty(this, "_pendingDrains", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: new Set()
        });
        Object.defineProperty(this, "langSmithToOTELTranslator", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "_tracingMode", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: "langsmith"
        });
        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: 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, "_customHeaders", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: {}
        });
        Object.defineProperty(this, "debug", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 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 ?? getLangSmithEnvironmentVariable("WORKSPACE_ID"));
        this.timeout_ms = config.timeout_ms ?? 90_000;
        this.caller = new 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 =
            getLangSmithEnvironmentVariable("FAILED_TRACES_DIR") || undefined;
        const failedTracesMb = 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 ?? DEFAULT_MAX_SIZE_BYTES;
        this.batchIngestCaller = new 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.hideMetadata = config.hideMetadata ?? defaultConfig.hideMetadata;
        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;
        this._tracingMode = resolveTracingMode(config.tracingMode);
        if (this._tracingMode === "otel") {
            this.langSmithToOTELTranslator = new LangSmithToOTELTranslator();
        }
        // Cache metadata env vars once during construction to avoid repeatedly scanning process.env
        this.cachedLSEnvVarsForMetadata = getLangSmithEnvVarsMetadata();
        // Initialize prompt cache
        // Handle backwards compatibility for deprecated `cache` parameter
        if (config.cache !== undefined && config.disablePromptCache) {
            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) {
            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 = promptCacheSingleton;
            }
            else {
                // Custom PromptCache instance provided
                this._promptCache = config.cache;
            }
        }
        else if (!config.disablePromptCache) {
            // Use the global singleton instance
            this._promptCache = promptCacheSingleton;
        }
        // Initialize custom headers
        this._customHeaders = config.headers ?? {};
    }
    static getDefaultClientConfig() {
        const apiKey = getLangSmithEnvironmentVariable("API_KEY");
        const apiUrl = getLangSmithEnvironmentVariable("ENDPOINT") ?? DEFAULT_API_URL;
        const hideInputs = getLangSmithEnvironmentVariable("HIDE_INPUTS") === "true";
        const hideOutputs = getLangSmithEnvironmentVariable("HIDE_OUTPUTS") === "true";
        const hideMetadata = getLangSmithEnvironmentVariable("HIDE_METADATA") === "true";
        return {
            apiUrl: apiUrl,
            apiKey: apiKey,
            webUrl: undefined,
            hideInputs: hideInputs,
            hideOutputs: hideOutputs,
            hideMetadata: hideMetadata,
        };
    }
    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("aws")) {
            this.webUrl = "https://aws.smith.langchain.com";
            return this.webUrl;
        }
        else if (this.apiUrl.split(".", 1)[0].includes("apac")) {
            this.webUrl = "https://apac.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 _mergedHeaders() {
        // Start with custom headers so they don't override required headers
        const headers = {
            "User-Agent": `langsmith-js/${__version__}`,
            ...this._customHeaders,
        };
        // Required headers that should not be overridden
        if (this.apiKey) {
            headers["x-api-key"] = `${this.apiKey}`;
        }
        if (this.workspaceId) {
            headers["x-tenant-id"] = this.workspaceId;
        }
        return headers;
    }
    /**
     * Get or set custom headers for the client.
     * Custom headers are merged with default headers (User-Agent, x-api-key, x-tenant-id).
     * Custom headers will not override the default required headers.
     */
    get headers() {
        return this._customHeaders;
    }
    set headers(value) {
        this._customHeaders = value ?? {};
    }
    _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 processMetadata(metadata) {
        if (this.hideMetadata === false) {
            return metadata;
        }
        if (this.hideMetadata === true) {
            return {};
        }
        if (typeof this.hideMetadata === "function") {
            return this.hideMetadata(metadata);
        }
        return metadata;
    }
    /**
     * Filter content from new_token events to prevent streaming LLM output
     * from being uploaded via events.
     */
    _filterNewTokenEvents(events) {
        if (!events || events.length === 0) {
            return events;
        }
        return events.map((event) => {
            if (event.name === "new_token") {
                // Remove the kwargs containing the token data
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                const { kwargs: _, ...rest } = event;
                return rest;
            }
            return event;
        });
    }
    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);
        }
        if (runParams.extra != null && "metadata" in runParams.extra) {
            runParams.extra = {
                ...runParams.extra,
                metadata: await this.processMetadata(runParams.extra.metadata),
            };
        }
        if (runParams.events !== undefined) {
            runParams.events = this._filterNewTokenEvents(runParams.events);
        }
        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._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await 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._mergedHeaders,
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                });
                await 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._mergedHeaders,
                        "Content-Type": "application/json",
                    },
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                    body,
                });
                await 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 ??
            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 (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) {
            this._trackDrain(this.drainAutoBatchQueue({
                batchSizeLimitBytes: sizeLimitBytes,
                batchSizeLimit: sizeLimit,
            }));
        }
        if (this.autoBatchQueue.items.length > 0) {
            this.autoBatchTimeout = setTimeout(() => {
                this.autoBatchTimeout = undefined;
                this._trackDrain(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: { ...this._mergedHeaders, Accept: "application/json" },
                signal: AbortSignal.timeout(SERVER_INFO_REQUEST_TIMEOUT_MS),
                ...this.fetchOptions,
            });
            await 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 = getOTELTrace();
        const otel_context = 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._mergedHeaders,
            "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 = serializePayloadForTracing(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 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(await this._serializeBody(batchChunks, `Ingesting runs with ids: ${runIds}`), options);
        }
    }
    async _postBatchIngestRuns(body, options) {
        const headers = {
            ...this._mergedHeaders,
            "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 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],
                        ...updateParam,
                    };
                }
                else {
                    standaloneUpdates.push(updateParam);
                }
            }
            preparedCreateParams = Object.values(createById);
            preparedUpdateParams = standaloneUpdates;
        }
        if (preparedCreateParams.length === 0 &&
            preparedUpdateParams.length === 0) {
            return;
        }
        // send the runs in multipart requests
        const accumulatedContext = [];
        const accumulatedParts = [];
        for (const [method, payloads] of [
            ["post", preparedCreateParams],
            ["patch", preparedUpdateParams],
        ]) {
            for (const originalPayload of payloads) {
                // collect fields to be sent as separate parts
                const { inputs, outputs, events, extra, error, serialized, attachments, ...payload } = originalPayload;
                const fields = { inputs, outputs, events, extra, error, serialized };
                // encode the main run payload
                const stringifiedPayload = await this._serializeBody(payload, `Serializing for multipart ingestion of run with id: ${payload.id}`);
                accumulatedParts.push({
                    name: `${method}.${payload.id}`,
                    payload: new Blob([stringifiedPayload], {
                        type: `application/json; length=${stringifiedPayload.length}`, // encoding=gzip
                    }),
                });
                // encode the fields we collected
                for (const [key, value] of Object.entries(fields)) {
                    if (value === undefined) {
                        continue;
                    }
                    const stringifiedValue = await this._serializeBody(value, `Serializing ${key} for multipart ingestion of run with id: ${payload.id}`);
                    accumulatedParts.push({
                        name: `${method}.${payload.id}.${key}`,
                        payload: new Blob([stringifiedValue], {
                            type: `application/json; length=${stringifiedValue.length}`,
                        }),
                    });
                }
                // encode the attachments
                if (payload.id !== undefined) {
                    const attachments = allAttachments[payload.id];
                    if (attachments) {
                        delete allAttachments[payload.id];
                        for (const [name, attachment] of Object.entries(attachments)) {
                            let contentType;
                            let content;
                            if (Array.isArray(attachment)) {
                                [contentType, content] = attachment;
                            }
                            else {
                                contentType = attachment.mimeType;
                                content = attachment.data;
                            }
                            // Validate that the attachment name doesn't contain a '.'
                            if (name.includes(".")) {
                                console.warn(`Skipping attachment '${name}' for run ${payload.id}: Invalid attachment name. ` +
                                    `Attachment names must not contain periods ('.'). Please rename the attachment and try again.`);
                                continue;
                            }
                            accumulatedParts.push({
                                name: `attachment.${payload.id}.${name}`,
                                payload: new Blob([content], {
                                    type: `${contentType}; length=${content.byteLength}`,
                                }),
                            });
                        }
                    }
                }
                // compute context
                accumulatedContext.push(`trace=${payload.trace_id},id=${payload.id}`);
            }
        }
        await this._sendMultipartRequest(accumulatedParts, accumulatedContext.join("; "), options);
    }
    async _createNodeFetchBody(parts, boundary) {
        // Create multipart form data manually using Blobs
        const chunks = [];
        for (const part of parts) {
            // Add field boundary
            chunks.push(new Blob([`--${boundary}\r\n`]));
            chunks.push(new Blob([
                `Content-Disposition: form-data; name="${part.name}"\r\n`,
                `Content-Type: ${part.payload.type}\r\n\r\n`,
            ]));
            chunks.push(part.payload);
            chunks.push(new Blob(["\r\n"]));
        }
        // Add final boundary
        chunks.push(new Blob([`--${boundary}--\r\n`]));
        // Combine all chunks into a single Blob
        const body = new Blob(chunks);
        // Convert Blob to ArrayBuffer for compatibility
        const arrayBuffer = await body.arrayBuffer();
        return arrayBuffer;
    }
    async _createMultipartStream(parts, boundary) {
        const encoder = new TextEncoder();
        // Create a ReadableStream for streaming the multipart data
        // Only do special handling if we're using node-fetch
        const stream = new ReadableStream({
            async start(controller) {
                // Helper function to write a chunk to the stream
                const writeChunk = async (chunk) => {
                    if (typeof chunk === "string") {
                        controller.enqueue(encoder.encode(chunk));
                    }
                    else {
                        controller.enqueue(chunk);
                    }
                };
                // Write each part to the stream
                for (const part of parts) {
                    // Write boundary and headers
                    await writeChunk(`--${boundary}\r\n`);
                    await writeChunk(`Content-Disposition: form-data; name="${part.name}"\r\n`);
                    await writeChunk(`Content-Type: ${part.payload.type}\r\n\r\n`);
                    // Write the payload
                    const payloadStream = part.payload.stream();
                    const reader = payloadStream.getReader();
                    try {
                        let result;
                        while (!(result = await reader.read()).done) {
                            controller.enqueue(result.value);
                        }
                    }
                    finally {
                        reader.releaseLock();
                    }
                    await writeChunk("\r\n");
                }
                // Write final boundary
                await writeChunk(`--${boundary}--\r\n`);
                controller.close();
            },
        });
        return stream;
    }
    async _sendMultipartRequest(parts, context, options) {
        // Create multipart form data boundary
        const boundary = "----LangSmithFormBoundary" + Math.random().toString(36).slice(2);
        const buildBuffered = () => this._createNodeFetchBody(parts, boundary);
        const buildStream = () => this._createMultipartStream(parts, boundary);
        const sendWithRetry = async (bodyFactory) => {
            return this.batchIngestCaller.callWithOptions({ sizeBytes: options?.sizeBytes }, async () => {
                const body = await bodyFactory();
                const headers = {
                    ...this._mergedHeaders,
                    "Content-Type": `multipart/form-data; boundary=${boundary}`,
                };
                if (options?.apiKey !== undefined) {
                    headers["x-api-key"] = options.apiKey;
                }
                let transformedBody = body;
                if (options?.useGzip &&
                    typeof body === "object" &&
                    "pipeThrough" in body) {
                    transformedBody = body.pipeThrough(new CompressionStream("gzip"));
                    headers["Content-Encoding"] = "gzip";
                }
                const response = await this._fetch(`${options?.apiUrl ?? this.apiUrl}/runs/multipart`, {
                    method: "POST",
                    headers,
                    body: transformedBody,
                    duplex: "half",
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                });
                await raiseForStatus(response, `Failed to send multipart request`, true);
                return response;
            });
        };
        try {
            let res;
            let streamedAttempt = false;
            const shouldStream = _shouldStreamForGlobalFetchImplementation();
            // attempt stream only if not disabled and not using node-fetch or Bun;
            if (shouldStream &&
                !this.multipartStreamingDisabled &&
                getEnv() !== "bun") {
                streamedAttempt = true;
                res = await sendWithRetry(buildStream);
            }
            else {
                res = await sendWithRetry(buildBuffered);
            }
            // if stream fails, fallback to buffered body
            if ((!this.multipartStreamingDisabled || streamedAttempt) &&
                res.status === 422 &&
                (options?.apiUrl ?? this.apiUrl) !== DEFAULT_API_URL) {
                console.warn(`Streaming multipart upload to ${options?.apiUrl ?? this.apiUrl}/runs/multipart failed. ` +
                    `This usually means the host does not support chunked uploads. ` +
                    `Retrying with a buffered upload for operation "${context}".`);
                // Disable streaming for future requests
                this.multipartStreamingDisabled = true;
                // retry with fully-buffered body
                res = await sendWithRetry(buildBuffered);
            }
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
        }
        catch (e) {
            // Re-throw 404 errors so caller can fall back to batch ingest
            if (isLangSmithNotFoundError(e)) {
                throw e;
            }
            console.warn(`${e.message.trim()}\n\nContext: ${context}`);
            if (this.failedTracesDir) {
                const bodyBuffer = await this._createNodeFetchBody(parts, boundary).catch(() => null);
                if (bodyBuffer) {
                    await Client._writeTraceToFallbackDir(this.failedTracesDir, bodyBuffer, { "Content-Type": `multipart/form-data; boundary=${boundary}` }, "runs/multipart", this.failedTracesMaxBytes);
                }
            }
        }
    }
    async updateRun(runId, run, options) {
        assertUuid(runId);
        if (run.inputs) {
            run.inputs = await this.processInputs(run.inputs);
        }
        if (run.outputs) {
            run.outputs = await this.processOutputs(run.outputs);
        }
        if (run.extra != null && "metadata" in run.extra) {
            run.extra = {
                ...run.extra,
                metadata: await this.processMetadata(run.extra.metadata),
            };
        }
        if (run.events) {
            run.events = this._filterNewTokenEvents(run.events);
        }
        // TODO: Untangle types
        const data = { ...run, id: runId };
        if (!this._filterForSampling([data], true).length) {
            return;
        }
        if (this.autoBatchTracing &&
            data.trace_id !== undefined &&
            data.dotted_order !== undefined) {
            const otelContext = this._cloneCurrentOTELContext();
            if (run.end_time !== undefined &&
                data.parent_run_id === undefined &&
                this.blockOnRootRunFinalization &&
                !this.manualFlushMode) {
                // Trigger batches as soon as a root trace ends and wait to ensure trace finishes
                // in serverless environments.
                await this.processRunOperation({
                    action: "update",
                    item: data,
                    otelContext,
                    apiKey: options?.apiKey,
                    apiUrl: options?.apiUrl,
                }).catch(console.error);
                return;
            }
            else {
                void this.processRunOperation({
                    action: "update",
                    item: data,
                    otelContext,
                    apiKey: options?.apiKey,
                    apiUrl: options?.apiUrl,
                }).catch(console.error);
            }
            return;
        }
        const headers = {
            ...this._mergedHeaders,
            "Content-Type": "application/json",
        };
        if (options?.apiKey !== undefined) {
            headers["x-api-key"] = options.apiKey;
        }
        if (options?.workspaceId !== undefined) {
            headers["x-tenant-id"] = options.workspaceId;
        }
        const body = serializePayloadForTracing(run, `Serializing payload to update run with id: ${runId}`);
        await this.caller.call(async () => {
            const res = await this._fetch(`${options?.apiUrl ?? this.apiUrl}/runs/${runId}`, {
                method: "PATCH",
                headers,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update run", true);
            return res;
        });
    }
    async readRun(runId, { loadChildRuns } = { loadChildRuns: false }) {
        assertUuid(runId);
        let run = _normalizeRunTimestamps(await this._get(`/runs/${runId}`));
        if (loadChildRuns) {
            run = await this._loadChildRuns(run);
        }
        return run;
    }
    async getRunUrl({ runId, run, projectOpts, }) {
        if (run !== undefined) {
            let sessionId;
            if (run.session_id) {
                sessionId = run.session_id;
            }
            else if (projectOpts?.projectName) {
                sessionId = (await this.readProject({ projectName: projectOpts?.projectName })).id;
            }
            else if (projectOpts?.projectId) {
                sessionId = projectOpts?.projectId;
            }
            else {
                const project = await this.readProject({
                    projectName: getLangSmithEnvironmentVariable("PROJECT") || "default",
                });
                sessionId = project.id;
            }
            const tenantId = await this._getTenantId();
            return `${this.getHostUrl()}/o/${tenantId}/projects/p/${sessionId}/r/${run.id}?poll=true`;
        }
        else if (runId !== undefined) {
            const run_ = await this.readRun(runId);
            if (!run_.app_path) {
                throw new Error(`Run ${runId} has no app_path`);
            }
            const baseUrl = this.getHostUrl();
            return `${baseUrl}${run_.app_path}`;
        }
        else {
            throw new Error("Must provide either runId or run");
        }
    }
    async _loadChildRuns(run) {
        const childRuns = await toArray(this.listRuns({
            isRoot: false,
            projectId: run.session_id,
            traceId: run.trace_id,
        }));
        const treemap = {};
        const runs = {};
        // TODO: make dotted order required when the migration finishes
        childRuns.sort((a, b) => (a?.dotted_order ?? "").localeCompare(b?.dotted_order ?? ""));
        for (const childRun of childRuns) {
            if (childRun.parent_run_id === null ||
                childRun.parent_run_id === undefined) {
                throw new Error(`Child run ${childRun.id} has no parent`);
            }
            if (childRun.dotted_order?.startsWith(run.dotted_order ?? "") &&
                childRun.id !== run.id) {
                if (!(childRun.parent_run_id in treemap)) {
                    treemap[childRun.parent_run_id] = [];
                }
                treemap[childRun.parent_run_id].push(childRun);
                runs[childRun.id] = childRun;
            }
        }
        run.child_runs = treemap[run.id] || [];
        for (const runId in treemap) {
            if (runId !== run.id) {
                runs[runId].child_runs = treemap[runId];
            }
        }
        return run;
    }
    /**
     * List runs from the LangSmith server.
     * @param projectId - The ID of the project to filter by.
     * @param projectName - The name of the project to filter by.
     * @param parentRunId - The ID of the parent run to filter by.
     * @param traceId - The ID of the trace to filter by.
     * @param referenceExampleId - The ID of the reference example to filter by.
     * @param startTime - The start time to filter by.
     * @param isRoot - Indicates whether to only return root runs.
     * @param runType - The run type to filter by.
     * @param error - Indicates whether to filter by error runs.
     * @param id - The ID of the run to filter by.
     * @param query - The query string to filter by.
     * @param filter - The filter string to apply to the run spans.
     * @param traceFilter - The filter string to apply on the root run of the trace.
     * @param treeFilter - The filter string to apply on other runs in the trace.
     * @param limit - The maximum number of runs to retrieve.
     * @returns {AsyncIterable<Run>} - The runs.
     *
     * @example
     * // List all runs in a project
     * const projectRuns = client.listRuns({ projectName: "<your_project>" });
     *
     * @example
     * // List LLM and Chat runs in the last 24 hours
     * const todaysLLMRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   start_time: new Date(Date.now() - 24 * 60 * 60 * 1000),
     *   run_type: "llm",
     * });
     *
     * @example
     * // List traces in a project
     * const rootRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   execution_order: 1,
     * });
     *
     * @example
     * // List runs without errors
     * const correctRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   error: false,
     * });
     *
     * @example
     * // List runs by run ID
     * const runIds = [
     *   "a36092d2-4ad5-4fb4-9c0d-0dba9a2ed836",
     *   "9398e6be-964f-4aa4-8ae9-ad78cd4b7074",
     * ];
     * const selectedRuns = client.listRuns({ run_ids: runIds });
     *
     * @example
     * // List all "chain" type runs that took more than 10 seconds and had `total_tokens` greater than 5000
     * const chainRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   filter: 'and(eq(run_type, "chain"), gt(latency, 10), gt(total_tokens, 5000))',
     * });
     *
     * @example
     * // List all runs called "extractor" whose root of the trace was assigned feedback "user_score" score of 1
     * const goodExtractorRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   filter: 'eq(name, "extractor")',
     *   traceFilter: 'and(eq(feedback_key, "user_score"), eq(feedback_score, 1))',
     * });
     *
     * @example
     * // List all runs that started after a specific timestamp and either have "error" not equal to null or a "Correctness" feedback score equal to 0
     * const complexRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   filter: 'and(gt(start_time, "2023-07-15T12:34:56Z"), or(neq(error, null), and(eq(feedback_key, "Correctness"), eq(feedback_score, 0.0))))',
     * });
     *
     * @example
     * // List all runs where `tags` include "experimental" or "beta" and `latency` is greater than 2 seconds
     * const taggedRuns = client.listRuns({
     *   projectName: "<your_project>",
     *   filter: 'and(or(has(tags, "experimental"), has(tags, "beta")), gt(latency, 2))',
     * });
     */
    async *listRuns(props) {
        const { projectId, projectName, parentRunId, traceId, referenceExampleId, startTime, executionOrder, isRoot, runType, error, id, query, filter, traceFilter, treeFilter, limit, select, order, } = props;
        let projectIds = [];
        if (projectId) {
            projectIds = Array.isArray(projectId) ? projectId : [projectId];
        }
        if (projectName) {
            const projectNames = Array.isArray(projectName)
                ? projectName
                : [projectName];
            const projectIds_ = await Promise.all(projectNames.map((name) => this.readProject({ projectName: name }).then((project) => project.id)));
            projectIds.push(...projectIds_);
        }
        const default_select = [
            "app_path",
            "completion_cost",
            "completion_tokens",
            "dotted_order",
            "end_time",
            "error",
            "events",
            "extra",
            "feedback_stats",
            "first_token_time",
            "id",
            "inputs",
            "name",
            "outputs",
            "parent_run_id",
            "parent_run_ids",
            "prompt_cost",
            "prompt_tokens",
            "reference_example_id",
            "run_type",
            "session_id",
            "start_time",
            "status",
            "tags",
            "total_cost",
            "total_tokens",
            "trace_id",
        ];
        const body = {
            session: projectIds.length ? projectIds : null,
            run_type: runType,
            reference_example: referenceExampleId,
            query,
            filter,
            trace_filter: traceFilter,
            tree_filter: treeFilter,
            execution_order: executionOrder,
            parent_run: parentRunId,
            start_time: startTime ? startTime.toISOString() : null,
            error,
            id,
            limit,
            trace: traceId,
            select: select ? select : default_select,
            is_root: isRoot,
            order,
        };
        if (body.select.includes("child_run_ids")) {
            warnOnce("Deprecated: 'child_run_ids' in the listRuns select parameter is deprecated and will be removed in a future version.");
        }
        let runsYielded = 0;
        for await (const runs of this._getCursorPaginatedList("/runs/query", body)) {
            const normalized = runs.map(_normalizeRunTimestamps);
            if (limit) {
                if (runsYielded >= limit) {
                    break;
                }
                if (normalized.length + runsYielded > limit) {
                    const newRuns = normalized.slice(0, limit - runsYielded);
                    yield* newRuns;
                    break;
                }
                runsYielded += normalized.length;
                yield* normalized;
            }
            else {
                yield* normalized;
            }
        }
    }
    async *listGroupRuns(props) {
        const { projectId, projectName, groupBy, filter, startTime, endTime, limit, offset, } = props;
        const sessionId = projectId || (await this.readProject({ projectName })).id;
        const baseBody = {
            session_id: sessionId,
            group_by: groupBy,
            filter,
            start_time: startTime ? startTime.toISOString() : null,
            end_time: endTime ? endTime.toISOString() : null,
            limit: Number(limit) || 100,
        };
        let currentOffset = Number(offset) || 0;
        const path = "/runs/group";
        const url = `${this.apiUrl}${path}`;
        while (true) {
            const currentBody = {
                ...baseBody,
                offset: currentOffset,
            };
            // Remove undefined values from the payload
            const filteredPayload = Object.fromEntries(Object.entries(currentBody).filter(([_, value]) => value !== undefined));
            const body = JSON.stringify(filteredPayload);
            const response = await this.caller.call(async () => {
                const res = await this._fetch(url, {
                    method: "POST",
                    headers: {
                        ...this._mergedHeaders,
                        "Content-Type": "application/json",
                    },
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                    body,
                });
                await raiseForStatus(res, `Failed to fetch ${path}`);
                return res;
            });
            const items = await response.json();
            const { groups, total } = items;
            if (groups.length === 0) {
                break;
            }
            for (const thread of groups) {
                yield thread;
            }
            currentOffset += groups.length;
            if (currentOffset >= total) {
                break;
            }
        }
    }
    async *readThread(props) {
        const { threadId, projectId, projectName, isRoot = true, limit, filter: userFilter, order = "asc", } = props;
        if (!projectId && !projectName) {
            throw new Error("threadId requires projectId or projectName");
        }
        const threadFilter = `eq(thread_id, ${JSON.stringify(threadId)})`;
        const combinedFilter = userFilter
            ? `and(${threadFilter}, ${userFilter})`
            : threadFilter;
        yield* this.listRuns({
            projectId: projectId ?? undefined,
            projectName: projectName ?? undefined,
            isRoot,
            limit,
            filter: combinedFilter,
            order,
        });
    }
    async listThreads(props) {
        const { projectId, projectName, limit, offset = 0, filter, startTime, isRoot = true, } = props;
        if (!projectId && !projectName) {
            throw new Error("Either projectId or projectName must be provided");
        }
        if (projectId && projectName) {
            throw new Error("Provide exactly one of projectId or projectName");
        }
        const sessionId = projectId ?? (await this.readProject({ projectName: projectName })).id;
        const startTimeResolved = startTime ?? new Date(Date.now() - 1 * 24 * 60 * 60 * 1000);
        const runSelect = [
            "id",
            "name",
            "status",
            "start_time",
            "end_time",
            "thread_id",
            "trace_id",
            "run_type",
            "error",
            "tags",
            "session_id",
            "parent_run_id",
            "total_tokens",
            "total_cost",
            "dotted_order",
            "reference_example_id",
            "feedback_stats",
            "app_path",
            "completion_cost",
            "completion_tokens",
            "prompt_cost",
            "prompt_tokens",
            "first_token_time",
        ];
        const bodyQuery = {
            session: [sessionId],
            is_root: isRoot,
            limit: 100,
            order: "desc",
            select: runSelect,
            start_time: startTimeResolved.toISOString(),
        };
        if (filter != null) {
            bodyQuery.filter = filter;
        }
        const threadsMap = new Map();
        for await (const runs of this._getCursorPaginatedList("/runs/query", bodyQuery)) {
            for (const raw of runs) {
                const run = _normalizeRunTimestamps(raw);
                const tid = run.thread_id;
                if (tid) {
                    const list = threadsMap.get(tid) ?? [];
                    list.push(run);
                    threadsMap.set(tid, list);
                }
            }
        }
        const result = [];
        for (const [threadId, runs] of threadsMap.entries()) {
            runs.sort((a, b) => {
                const aRun = a;
                const bRun = b;
                const aStart = aRun.start_time ?? "";
                const bStart = bRun.start_time ?? "";
                if (aStart !== bStart)
                    return aStart.localeCompare(bStart);
                const aOrder = aRun.dotted_order ?? "";
                const bOrder = bRun.dotted_order ?? "";
                return aOrder.localeCompare(bOrder);
            });
            const startTimes = runs
                .map((r) => r.start_time)
                .filter(Boolean);
            const sortedTimes = [...startTimes].sort();
            const minStart = sortedTimes.length ? sortedTimes[0] : "";
            const maxStart = sortedTimes.length
                ? sortedTimes[sortedTimes.length - 1]
                : "";
            result.push({
                thread_id: threadId,
                runs,
                count: runs.length,
                filter: "",
                total_tokens: 0,
                total_cost: null,
                min_start_time: minStart,
                max_start_time: maxStart,
                latency_p50: 0,
                latency_p99: 0,
                feedback_stats: null,
                first_inputs: "",
                last_outputs: "",
                last_error: null,
            });
        }
        result.sort((a, b) => {
            const aMax = a.max_start_time ?? "";
            const bMax = b.max_start_time ?? "";
            return bMax.localeCompare(aMax);
        });
        const withOffset = offset > 0 ? result.slice(offset) : result;
        const withLimit = limit !== undefined ? withOffset.slice(0, limit) : withOffset;
        return withLimit;
    }
    async getRunStats({ id, trace, parentRun, runType, projectNames, projectIds, referenceExampleIds, startTime, endTime, error, query, filter, traceFilter, treeFilter, isRoot, dataSourceType, }) {
        let projectIds_ = projectIds || [];
        if (projectNames) {
            projectIds_ = [
                ...(projectIds || []),
                ...(await Promise.all(projectNames.map((name) => this.readProject({ projectName: name }).then((project) => project.id)))),
            ];
        }
        const payload = {
            id,
            trace,
            parent_run: parentRun,
            run_type: runType,
            session: projectIds_,
            reference_example: referenceExampleIds,
            start_time: startTime,
            end_time: endTime,
            error,
            query,
            filter,
            trace_filter: traceFilter,
            tree_filter: treeFilter,
            is_root: isRoot,
            data_source_type: dataSourceType,
        };
        // Remove undefined values from the payload
        const filteredPayload = Object.fromEntries(Object.entries(payload).filter(([_, value]) => value !== undefined));
        const body = JSON.stringify(filteredPayload);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/runs/stats`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "get run stats");
            return res;
        });
        const result = await response.json();
        return result;
    }
    async shareRun(runId, { shareId } = {}) {
        const data = {
            run_id: runId,
            share_token: shareId || uuid.v4(),
        };
        assertUuid(runId);
        const body = JSON.stringify(data);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/runs/${runId}/share`, {
                method: "PUT",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "share run");
            return res;
        });
        const result = await response.json();
        if (result === null || !("share_token" in result)) {
            throw new Error("Invalid response from server");
        }
        return `${this.getHostUrl()}/public/${result["share_token"]}/r`;
    }
    async unshareRun(runId) {
        assertUuid(runId);
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/runs/${runId}/share`, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "unshare run", true);
            return res;
        });
    }
    async readRunSharedLink(runId) {
        assertUuid(runId);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/runs/${runId}/share`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "read run shared link");
            return res;
        });
        const result = await response.json();
        if (result === null || !("share_token" in result)) {
            return undefined;
        }
        return `${this.getHostUrl()}/public/${result["share_token"]}/r`;
    }
    async listSharedRuns(shareToken, { runIds, } = {}) {
        const queryParams = new URLSearchParams({
            share_token: shareToken,
        });
        if (runIds !== undefined) {
            for (const runId of runIds) {
                queryParams.append("id", runId);
            }
        }
        assertUuid(shareToken);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/public/${shareToken}/runs${queryParams}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "list shared runs");
            return res;
        });
        const runs = (await response.json());
        return runs.map(_normalizeRunTimestamps);
    }
    async readDatasetSharedSchema(datasetId, datasetName) {
        if (!datasetId && !datasetName) {
            throw new Error("Either datasetId or datasetName must be given");
        }
        if (!datasetId) {
            const dataset = await this.readDataset({ datasetName });
            datasetId = dataset.id;
        }
        assertUuid(datasetId);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${datasetId}/share`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "read dataset shared schema");
            return res;
        });
        const shareSchema = await response.json();
        shareSchema.url = `${this.getHostUrl()}/public/${shareSchema.share_token}/d`;
        return shareSchema;
    }
    async shareDataset(datasetId, datasetName) {
        if (!datasetId && !datasetName) {
            throw new Error("Either datasetId or datasetName must be given");
        }
        if (!datasetId) {
            const dataset = await this.readDataset({ datasetName });
            datasetId = dataset.id;
        }
        const data = {
            dataset_id: datasetId,
        };
        assertUuid(datasetId);
        const body = JSON.stringify(data);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${datasetId}/share`, {
                method: "PUT",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "share dataset");
            return res;
        });
        const shareSchema = await response.json();
        shareSchema.url = `${this.getHostUrl()}/public/${shareSchema.share_token}/d`;
        return shareSchema;
    }
    async unshareDataset(datasetId) {
        assertUuid(datasetId);
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${datasetId}/share`, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "unshare dataset", true);
            return res;
        });
    }
    async readSharedDataset(shareToken) {
        assertUuid(shareToken);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/public/${shareToken}/datasets`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "read shared dataset");
            return res;
        });
        const dataset = await response.json();
        return dataset;
    }
    /**
     * Get shared examples.
     *
     * @param {string} shareToken The share token to get examples for. A share token is the UUID (or LangSmith URL, including UUID) generated when explicitly marking an example as public.
     * @param {Object} [options] Additional options for listing the examples.
     * @param {string[] | undefined} [options.exampleIds] A list of example IDs to filter by.
     * @returns {Promise<Example[]>} The shared examples.
     */
    async listSharedExamples(shareToken, options) {
        const params = {};
        if (options?.exampleIds) {
            params.id = options.exampleIds;
        }
        const urlParams = new URLSearchParams();
        Object.entries(params).forEach(([key, value]) => {
            if (Array.isArray(value)) {
                value.forEach((v) => urlParams.append(key, v));
            }
            else {
                urlParams.append(key, value);
            }
        });
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/public/${shareToken}/examples?${urlParams.toString()}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "list shared examples");
            return res;
        });
        const result = await response.json();
        if (!response.ok) {
            if ("detail" in result) {
                throw new Error(`Failed to list shared examples.\nStatus: ${response.status}\nMessage: ${Array.isArray(result.detail)
                    ? result.detail.join("\n")
                    : "Unspecified error"}`);
            }
            throw new Error(`Failed to list shared examples: ${response.status} ${response.statusText}`);
        }
        return result.map((example) => ({
            ...example,
            _hostUrl: this.getHostUrl(),
        }));
    }
    async createProject({ projectName, description = null, metadata = null, upsert = false, projectExtra = null, referenceDatasetId = null, }) {
        const upsert_ = upsert ? `?upsert=true` : "";
        const endpoint = `${this.apiUrl}/sessions${upsert_}`;
        const extra = projectExtra || {};
        if (metadata) {
            extra["metadata"] = metadata;
        }
        const body = {
            name: projectName,
            extra,
            description,
        };
        if (referenceDatasetId !== null) {
            body["reference_dataset_id"] = referenceDatasetId;
        }
        const serializedBody = JSON.stringify(body);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(endpoint, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: serializedBody,
            });
            await raiseForStatus(res, "create project");
            return res;
        });
        const result = await response.json();
        return result;
    }
    async updateProject(projectId, { name = null, description = null, metadata = null, projectExtra = null, endTime = null, }) {
        const endpoint = `${this.apiUrl}/sessions/${projectId}`;
        let extra = projectExtra;
        if (metadata) {
            extra = { ...(extra || {}), metadata };
        }
        const body = JSON.stringify({
            name,
            extra,
            description,
            end_time: endTime ? new Date(endTime).toISOString() : null,
        });
        const response = await this.caller.call(async () => {
            const res = await this._fetch(endpoint, {
                method: "PATCH",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update project");
            return res;
        });
        const result = await response.json();
        return result;
    }
    async hasProject({ projectId, projectName, }) {
        // TODO: Add a head request
        let path = "/sessions";
        const params = new URLSearchParams();
        if (projectId !== undefined && projectName !== undefined) {
            throw new Error("Must provide either projectName or projectId, not both");
        }
        else if (projectId !== undefined) {
            assertUuid(projectId);
            path += `/${projectId}`;
        }
        else if (projectName !== undefined) {
            params.append("name", projectName);
        }
        else {
            throw new Error("Must provide projectName or projectId");
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}${path}?${params}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "has project");
            return res;
        });
        // consume the response body to release the connection
        // https://undici.nodejs.org/#/?id=garbage-collection
        try {
            const result = await response.json();
            if (!response.ok) {
                return false;
            }
            // If it's OK and we're querying by name, need to check the list is not empty
            if (Array.isArray(result)) {
                return result.length > 0;
            }
            // projectId querying
            return true;
        }
        catch (_e) {
            return false;
        }
    }
    async readProject({ projectId, projectName, includeStats, }) {
        let path = "/sessions";
        const params = new URLSearchParams();
        if (projectId !== undefined && projectName !== undefined) {
            throw new Error("Must provide either projectName or projectId, not both");
        }
        else if (projectId !== undefined) {
            assertUuid(projectId);
            path += `/${projectId}`;
        }
        else if (projectName !== undefined) {
            params.append("name", projectName);
        }
        else {
            throw new Error("Must provide projectName or projectId");
        }
        if (includeStats !== undefined) {
            params.append("include_stats", includeStats.toString());
        }
        const response = await this._get(path, params);
        let result;
        if (Array.isArray(response)) {
            if (response.length === 0) {
                throw new Error(`Project[id=${projectId}, name=${projectName}] not found`);
            }
            result = response[0];
        }
        else {
            result = response;
        }
        return result;
    }
    async getProjectUrl({ projectId, projectName, }) {
        if (projectId === undefined && projectName === undefined) {
            throw new Error("Must provide either projectName or projectId");
        }
        const project = await this.readProject({ projectId, projectName });
        const tenantId = await this._getTenantId();
        return `${this.getHostUrl()}/o/${tenantId}/projects/p/${project.id}`;
    }
    async getDatasetUrl({ datasetId, datasetName, }) {
        if (datasetId === undefined && datasetName === undefined) {
            throw new Error("Must provide either datasetName or datasetId");
        }
        const dataset = await this.readDataset({ datasetId, datasetName });
        const tenantId = await this._getTenantId();
        return `${this.getHostUrl()}/o/${tenantId}/datasets/${dataset.id}`;
    }
    async _getTenantId() {
        if (this._tenantId !== null) {
            return this._tenantId;
        }
        const queryParams = new URLSearchParams({ limit: "1" });
        for await (const projects of this._getPaginated("/sessions", queryParams)) {
            this._tenantId = projects[0].tenant_id;
            return projects[0].tenant_id;
        }
        throw new Error("No projects found to resolve tenant.");
    }
    async *listProjects({ projectIds, name, nameContains, referenceDatasetId, referenceDatasetName, includeStats, datasetVersion, referenceFree, metadata, } = {}) {
        const params = new URLSearchParams();
        if (projectIds !== undefined) {
            for (const projectId of projectIds) {
                params.append("id", projectId);
            }
        }
        if (name !== undefined) {
            params.append("name", name);
        }
        if (nameContains !== undefined) {
            params.append("name_contains", nameContains);
        }
        if (referenceDatasetId !== undefined) {
            params.append("reference_dataset", referenceDatasetId);
        }
        else if (referenceDatasetName !== undefined) {
            const dataset = await this.readDataset({
                datasetName: referenceDatasetName,
            });
            params.append("reference_dataset", dataset.id);
        }
        if (includeStats !== undefined) {
            params.append("include_stats", includeStats.toString());
        }
        if (datasetVersion !== undefined) {
            params.append("dataset_version", datasetVersion);
        }
        if (referenceFree !== undefined) {
            params.append("reference_free", referenceFree.toString());
        }
        if (metadata !== undefined) {
            params.append("metadata", JSON.stringify(metadata));
        }
        for await (const projects of this._getPaginated("/sessions", params)) {
            yield* projects;
        }
    }
    async deleteProject({ projectId, projectName, }) {
        let projectId_;
        if (projectId === undefined && projectName === undefined) {
            throw new Error("Must provide projectName or projectId");
        }
        else if (projectId !== undefined && projectName !== undefined) {
            throw new Error("Must provide either projectName or projectId, not both");
        }
        else if (projectId === undefined) {
            projectId_ = (await this.readProject({ projectName })).id;
        }
        else {
            projectId_ = projectId;
        }
        assertUuid(projectId_);
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/sessions/${projectId_}`, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, `delete session ${projectId_} (${projectName})`, true);
            return res;
        });
    }
    async uploadCsv({ csvFile, fileName, inputKeys, outputKeys, description, dataType, name, }) {
        const url = `${this.apiUrl}/datasets/upload`;
        const formData = new FormData();
        const csvBlob = new Blob([csvFile], { type: "text/csv" });
        formData.append("file", csvBlob, fileName);
        inputKeys.forEach((key) => {
            formData.append("input_keys", key);
        });
        outputKeys.forEach((key) => {
            formData.append("output_keys", key);
        });
        if (description) {
            formData.append("description", description);
        }
        if (dataType) {
            formData.append("data_type", dataType);
        }
        if (name) {
            formData.append("name", name);
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(url, {
                method: "POST",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: formData,
            });
            await raiseForStatus(res, "upload CSV");
            return res;
        });
        const result = await response.json();
        return result;
    }
    async createDataset(name, { description, dataType, inputsSchema, outputsSchema, metadata, } = {}) {
        const body = {
            name,
            description,
            extra: { source: "sdk", ...(metadata ? { metadata } : {}) },
        };
        if (dataType) {
            body.data_type = dataType;
        }
        if (inputsSchema) {
            body.inputs_schema_definition = inputsSchema;
        }
        if (outputsSchema) {
            body.outputs_schema_definition = outputsSchema;
        }
        const serializedBody = JSON.stringify(body);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: serializedBody,
            });
            await raiseForStatus(res, "create dataset");
            return res;
        });
        const result = await response.json();
        return result;
    }
    async readDataset({ datasetId, datasetName, }) {
        let path = "/datasets";
        // limit to 1 result
        const params = new URLSearchParams({ limit: "1" });
        if (datasetId && datasetName) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId) {
            assertUuid(datasetId);
            path += `/${datasetId}`;
        }
        else if (datasetName) {
            params.append("name", datasetName);
        }
        else {
            throw new Error("Must provide datasetName or datasetId");
        }
        const response = await this._get(path, params);
        let result;
        if (Array.isArray(response)) {
            if (response.length === 0) {
                throw new Error(`Dataset[id=${datasetId}, name=${datasetName}] not found`);
            }
            result = response[0];
        }
        else {
            result = response;
        }
        return result;
    }
    async hasDataset({ datasetId, datasetName, }) {
        try {
            await this.readDataset({ datasetId, datasetName });
            return true;
        }
        catch (e) {
            if (
            // eslint-disable-next-line no-instanceof/no-instanceof
            e instanceof Error &&
                e.message.toLocaleLowerCase().includes("not found")) {
                return false;
            }
            throw e;
        }
    }
    async diffDatasetVersions({ datasetId, datasetName, fromVersion, toVersion, }) {
        let datasetId_ = datasetId;
        if (datasetId_ === undefined && datasetName === undefined) {
            throw new Error("Must provide either datasetName or datasetId");
        }
        else if (datasetId_ !== undefined && datasetName !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId_ === undefined) {
            const dataset = await this.readDataset({ datasetName });
            datasetId_ = dataset.id;
        }
        const urlParams = new URLSearchParams({
            from_version: typeof fromVersion === "string"
                ? fromVersion
                : fromVersion.toISOString(),
            to_version: typeof toVersion === "string" ? toVersion : toVersion.toISOString(),
        });
        const response = await this._get(`/datasets/${datasetId_}/versions/diff`, urlParams);
        return response;
    }
    async readDatasetOpenaiFinetuning({ datasetId, datasetName, }) {
        const path = "/datasets";
        if (datasetId !== undefined) {
            // do nothing
        }
        else if (datasetName !== undefined) {
            datasetId = (await this.readDataset({ datasetName })).id;
        }
        else {
            throw new Error("Must provide either datasetName or datasetId");
        }
        const response = await this._getResponse(`${path}/${datasetId}/openai_ft`);
        const datasetText = await response.text();
        const dataset = datasetText
            .trim()
            .split("\n")
            .map((line) => JSON.parse(line));
        return dataset;
    }
    async *listDatasets({ limit = 100, offset = 0, datasetIds, datasetName, datasetNameContains, metadata, } = {}) {
        const path = "/datasets";
        const params = new URLSearchParams({
            limit: limit.toString(),
            offset: offset.toString(),
        });
        if (datasetIds !== undefined) {
            for (const id_ of datasetIds) {
                params.append("id", id_);
            }
        }
        if (datasetName !== undefined) {
            params.append("name", datasetName);
        }
        if (datasetNameContains !== undefined) {
            params.append("name_contains", datasetNameContains);
        }
        if (metadata !== undefined) {
            params.append("metadata", JSON.stringify(metadata));
        }
        for await (const datasets of this._getPaginated(path, params)) {
            yield* datasets;
        }
    }
    /**
     * Update a dataset
     * @param props The dataset details to update
     * @returns The updated dataset
     */
    async updateDataset(props) {
        const { datasetId, datasetName, ...update } = props;
        if (!datasetId && !datasetName) {
            throw new Error("Must provide either datasetName or datasetId");
        }
        const _datasetId = datasetId ?? (await this.readDataset({ datasetName })).id;
        assertUuid(_datasetId);
        const body = JSON.stringify(update);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${_datasetId}`, {
                method: "PATCH",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update dataset");
            return res;
        });
        return (await response.json());
    }
    /**
     * Updates a tag on a dataset.
     *
     * If the tag is already assigned to a different version of this dataset,
     * the tag will be moved to the new version. The as_of parameter is used to
     * determine which version of the dataset to apply the new tags to.
     *
     * It must be an exact version of the dataset to succeed. You can
     * use the "readDatasetVersion" method to find the exact version
     * to apply the tags to.
     * @param params.datasetId The ID of the dataset to update. Must be provided if "datasetName" is not provided.
     * @param params.datasetName The name of the dataset to update. Must be provided if "datasetId" is not provided.
     * @param params.asOf The timestamp of the dataset to apply the new tags to.
     * @param params.tag The new tag to apply to the dataset.
     */
    async updateDatasetTag(props) {
        const { datasetId, datasetName, asOf, tag } = props;
        if (!datasetId && !datasetName) {
            throw new Error("Must provide either datasetName or datasetId");
        }
        const _datasetId = datasetId ?? (await this.readDataset({ datasetName })).id;
        assertUuid(_datasetId);
        const body = JSON.stringify({
            as_of: typeof asOf === "string" ? asOf : asOf.toISOString(),
            tag,
        });
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${_datasetId}/tags`, {
                method: "PUT",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update dataset tags", true);
            return res;
        });
    }
    async deleteDataset({ datasetId, datasetName, }) {
        let path = "/datasets";
        let datasetId_ = datasetId;
        if (datasetId !== undefined && datasetName !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetName !== undefined) {
            const dataset = await this.readDataset({ datasetName });
            datasetId_ = dataset.id;
        }
        if (datasetId_ !== undefined) {
            assertUuid(datasetId_);
            path += `/${datasetId_}`;
        }
        else {
            throw new Error("Must provide datasetName or datasetId");
        }
        await this.caller.call(async () => {
            const res = await this._fetch(this.apiUrl + path, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, `delete ${path}`, true);
            return res;
        });
    }
    async createExample(inputsOrUpdate, outputs, options) {
        if (isExampleCreate(inputsOrUpdate)) {
            if (outputs !== undefined || options !== undefined) {
                throw new Error("Cannot provide outputs or options when using ExampleCreate object");
            }
        }
        let datasetId_ = outputs ? options?.datasetId : inputsOrUpdate.dataset_id;
        const datasetName_ = outputs
            ? options?.datasetName
            : inputsOrUpdate.dataset_name;
        if (datasetId_ === undefined && datasetName_ === undefined) {
            throw new Error("Must provide either datasetName or datasetId");
        }
        else if (datasetId_ !== undefined && datasetName_ !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId_ === undefined) {
            const dataset = await this.readDataset({ datasetName: datasetName_ });
            datasetId_ = dataset.id;
        }
        const createdAt_ = (outputs ? options?.createdAt : inputsOrUpdate.created_at) || new Date();
        let data;
        if (!isExampleCreate(inputsOrUpdate)) {
            data = {
                inputs: inputsOrUpdate,
                outputs,
                created_at: createdAt_?.toISOString(),
                id: options?.exampleId,
                metadata: options?.metadata,
                split: options?.split,
                source_run_id: options?.sourceRunId,
                use_source_run_io: options?.useSourceRunIO,
                use_source_run_attachments: options?.useSourceRunAttachments,
                attachments: options?.attachments,
            };
        }
        else {
            data = inputsOrUpdate;
        }
        const response = await this._uploadExamplesMultipart(datasetId_, [data]);
        const example = await this.readExample(response.example_ids?.[0] ?? uuid.v4());
        return example;
    }
    async createExamples(propsOrUploads) {
        if (Array.isArray(propsOrUploads)) {
            if (propsOrUploads.length === 0) {
                return [];
            }
            const uploads = propsOrUploads;
            let datasetId_ = uploads[0].dataset_id;
            const datasetName_ = uploads[0].dataset_name;
            if (datasetId_ === undefined && datasetName_ === undefined) {
                throw new Error("Must provide either datasetName or datasetId");
            }
            else if (datasetId_ !== undefined && datasetName_ !== undefined) {
                throw new Error("Must provide either datasetName or datasetId, not both");
            }
            else if (datasetId_ === undefined) {
                const dataset = await this.readDataset({ datasetName: datasetName_ });
                datasetId_ = dataset.id;
            }
            const response = await this._uploadExamplesMultipart(datasetId_, uploads);
            const examples = await Promise.all(response.example_ids.map((id) => this.readExample(id)));
            return examples;
        }
        const { inputs, outputs, metadata, splits, sourceRunIds, useSourceRunIOs, useSourceRunAttachments, attachments, exampleIds, datasetId, datasetName, } = propsOrUploads;
        if (inputs === undefined) {
            throw new Error("Must provide inputs when using legacy parameters");
        }
        let datasetId_ = datasetId;
        const datasetName_ = datasetName;
        if (datasetId_ === undefined && datasetName_ === undefined) {
            throw new Error("Must provide either datasetName or datasetId");
        }
        else if (datasetId_ !== undefined && datasetName_ !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId_ === undefined) {
            const dataset = await this.readDataset({ datasetName: datasetName_ });
            datasetId_ = dataset.id;
        }
        const formattedExamples = inputs.map((input, idx) => {
            return {
                dataset_id: datasetId_,
                inputs: input,
                outputs: outputs?.[idx],
                metadata: metadata?.[idx],
                split: splits?.[idx],
                id: exampleIds?.[idx],
                attachments: attachments?.[idx],
                source_run_id: sourceRunIds?.[idx],
                use_source_run_io: useSourceRunIOs?.[idx],
                use_source_run_attachments: useSourceRunAttachments?.[idx],
            };
        });
        const response = await this._uploadExamplesMultipart(datasetId_, formattedExamples);
        const examples = await Promise.all(response.example_ids.map((id) => this.readExample(id)));
        return examples;
    }
    async createLLMExample(input, generation, options) {
        return this.createExample({ input }, { output: generation }, options);
    }
    async createChatExample(input, generations, options) {
        const finalInput = input.map((message) => {
            if (isLangChainMessage(message)) {
                return convertLangChainMessageToExample(message);
            }
            return message;
        });
        const finalOutput = isLangChainMessage(generations)
            ? convertLangChainMessageToExample(generations)
            : generations;
        return this.createExample({ input: finalInput }, { output: finalOutput }, options);
    }
    async readExample(exampleId) {
        assertUuid(exampleId);
        const path = `/examples/${exampleId}`;
        const rawExample = await this._get(path);
        const { attachment_urls, ...rest } = rawExample;
        const example = rest;
        if (attachment_urls) {
            example.attachments = Object.entries(attachment_urls).reduce((acc, [key, value]) => {
                acc[key.slice("attachment.".length)] = {
                    presigned_url: value.presigned_url,
                    mime_type: value.mime_type,
                };
                return acc;
            }, {});
        }
        return example;
    }
    async *listExamples({ datasetId, datasetName, exampleIds, asOf, splits, inlineS3Urls, metadata, limit, offset, filter, includeAttachments, } = {}) {
        let datasetId_;
        if (datasetId !== undefined && datasetName !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId !== undefined) {
            datasetId_ = datasetId;
        }
        else if (datasetName !== undefined) {
            const dataset = await this.readDataset({ datasetName });
            datasetId_ = dataset.id;
        }
        else {
            throw new Error("Must provide a datasetName or datasetId");
        }
        const params = new URLSearchParams({ dataset: datasetId_ });
        const dataset_version = asOf
            ? typeof asOf === "string"
                ? asOf
                : asOf?.toISOString()
            : undefined;
        if (dataset_version) {
            params.append("as_of", dataset_version);
        }
        const inlineS3Urls_ = inlineS3Urls ?? true;
        params.append("inline_s3_urls", inlineS3Urls_.toString());
        if (exampleIds !== undefined) {
            for (const id_ of exampleIds) {
                params.append("id", id_);
            }
        }
        if (splits !== undefined) {
            for (const split of splits) {
                params.append("splits", split);
            }
        }
        if (metadata !== undefined) {
            const serializedMetadata = JSON.stringify(metadata);
            params.append("metadata", serializedMetadata);
        }
        if (limit !== undefined) {
            params.append("limit", limit.toString());
        }
        if (offset !== undefined) {
            params.append("offset", offset.toString());
        }
        if (filter !== undefined) {
            params.append("filter", filter);
        }
        if (includeAttachments === true) {
            ["attachment_urls", "outputs", "metadata"].forEach((field) => params.append("select", field));
        }
        let i = 0;
        for await (const rawExamples of this._getPaginated("/examples", params)) {
            for (const rawExample of rawExamples) {
                const { attachment_urls, ...rest } = rawExample;
                const example = rest;
                if (attachment_urls) {
                    example.attachments = Object.entries(attachment_urls).reduce((acc, [key, value]) => {
                        acc[key.slice("attachment.".length)] = {
                            presigned_url: value.presigned_url,
                            mime_type: value.mime_type || undefined,
                        };
                        return acc;
                    }, {});
                }
                yield example;
                i++;
            }
            if (limit !== undefined && i >= limit) {
                break;
            }
        }
    }
    async deleteExample(exampleId) {
        assertUuid(exampleId);
        const path = `/examples/${exampleId}`;
        await this.caller.call(async () => {
            const res = await this._fetch(this.apiUrl + path, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, `delete ${path}`, true);
            return res;
        });
    }
    /**
     * Delete multiple examples by ID.
     * @param exampleIds - The IDs of the examples to delete
     * @param options - Optional settings for deletion
     * @param options.hardDelete - If true, permanently delete examples. If false (default), soft delete them.
     */
    async deleteExamples(exampleIds, options) {
        // Validate all UUIDs
        exampleIds.forEach((id) => assertUuid(id));
        if (options?.hardDelete) {
            // Hard delete uses POST to a different platform endpoint
            const path = this._getPlatformEndpointPath("datasets/examples/delete");
            await this.caller.call(async () => {
                const res = await this._fetch(`${this.apiUrl}${path}`, {
                    method: "POST",
                    headers: {
                        ...this._mergedHeaders,
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                        example_ids: exampleIds,
                        hard_delete: true,
                    }),
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                });
                await raiseForStatus(res, "hard delete examples", true);
                return res;
            });
        }
        else {
            // Soft delete uses DELETE with query params
            const params = new URLSearchParams();
            exampleIds.forEach((id) => params.append("example_ids", id));
            await this.caller.call(async () => {
                const res = await this._fetch(`${this.apiUrl}/examples?${params.toString()}`, {
                    method: "DELETE",
                    headers: this._mergedHeaders,
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                });
                await raiseForStatus(res, "delete examples", true);
                return res;
            });
        }
    }
    async updateExample(exampleIdOrUpdate, update) {
        let exampleId;
        if (update) {
            exampleId = exampleIdOrUpdate;
        }
        else {
            exampleId = exampleIdOrUpdate.id;
        }
        assertUuid(exampleId);
        let updateToUse;
        if (update) {
            updateToUse = { id: exampleId, ...update };
        }
        else {
            updateToUse = exampleIdOrUpdate;
        }
        let datasetId;
        if (updateToUse.dataset_id !== undefined) {
            datasetId = updateToUse.dataset_id;
        }
        else {
            const example = await this.readExample(exampleId);
            datasetId = example.dataset_id;
        }
        return this._updateExamplesMultipart(datasetId, [updateToUse]);
    }
    async updateExamples(update) {
        // We will naively get dataset id from first example and assume it works for all
        let datasetId;
        if (update[0].dataset_id === undefined) {
            const example = await this.readExample(update[0].id);
            datasetId = example.dataset_id;
        }
        else {
            datasetId = update[0].dataset_id;
        }
        return this._updateExamplesMultipart(datasetId, update);
    }
    /**
     * Get dataset version by closest date or exact tag.
     *
     * Use this to resolve the nearest version to a given timestamp or for a given tag.
     *
     * @param options The options for getting the dataset version
     * @param options.datasetId The ID of the dataset
     * @param options.datasetName The name of the dataset
     * @param options.asOf The timestamp of the dataset to retrieve
     * @param options.tag The tag of the dataset to retrieve
     * @returns The dataset version
     */
    async readDatasetVersion({ datasetId, datasetName, asOf, tag, }) {
        let resolvedDatasetId;
        if (!datasetId) {
            const dataset = await this.readDataset({ datasetName });
            resolvedDatasetId = dataset.id;
        }
        else {
            resolvedDatasetId = datasetId;
        }
        assertUuid(resolvedDatasetId);
        if ((asOf && tag) || (!asOf && !tag)) {
            throw new Error("Exactly one of asOf and tag must be specified.");
        }
        const params = new URLSearchParams();
        if (asOf !== undefined) {
            params.append("as_of", typeof asOf === "string" ? asOf : asOf.toISOString());
        }
        if (tag !== undefined) {
            params.append("tag", tag);
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${resolvedDatasetId}/version?${params.toString()}`, {
                method: "GET",
                headers: { ...this._mergedHeaders },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "read dataset version");
            return res;
        });
        return await response.json();
    }
    async listDatasetSplits({ datasetId, datasetName, asOf, }) {
        let datasetId_;
        if (datasetId === undefined && datasetName === undefined) {
            throw new Error("Must provide dataset name or ID");
        }
        else if (datasetId !== undefined && datasetName !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId === undefined) {
            const dataset = await this.readDataset({ datasetName });
            datasetId_ = dataset.id;
        }
        else {
            datasetId_ = datasetId;
        }
        assertUuid(datasetId_);
        const params = new URLSearchParams();
        const dataset_version = asOf
            ? typeof asOf === "string"
                ? asOf
                : asOf?.toISOString()
            : undefined;
        if (dataset_version) {
            params.append("as_of", dataset_version);
        }
        const response = await this._get(`/datasets/${datasetId_}/splits`, params);
        return response;
    }
    async updateDatasetSplits({ datasetId, datasetName, splitName, exampleIds, remove = false, }) {
        let datasetId_;
        if (datasetId === undefined && datasetName === undefined) {
            throw new Error("Must provide dataset name or ID");
        }
        else if (datasetId !== undefined && datasetName !== undefined) {
            throw new Error("Must provide either datasetName or datasetId, not both");
        }
        else if (datasetId === undefined) {
            const dataset = await this.readDataset({ datasetName });
            datasetId_ = dataset.id;
        }
        else {
            datasetId_ = datasetId;
        }
        assertUuid(datasetId_);
        const data = {
            split_name: splitName,
            examples: exampleIds.map((id) => {
                assertUuid(id);
                return id;
            }),
            remove,
        };
        const body = JSON.stringify(data);
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/${datasetId_}/splits`, {
                method: "PUT",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update dataset splits", true);
            return res;
        });
    }
    async createFeedback(runId, key, { score, value, correction, comment, sourceInfo, feedbackSourceType = "api", sourceRunId, feedbackId, feedbackConfig, projectId, comparativeExperimentId, sessionId, startTime, }) {
        if (!runId && !projectId) {
            throw new Error("One of runId or projectId must be provided");
        }
        if (runId && projectId) {
            throw new Error("Only one of runId or projectId can be provided");
        }
        const feedback_source = {
            type: feedbackSourceType ?? "api",
            metadata: sourceInfo ?? {},
        };
        if (sourceRunId !== undefined &&
            feedback_source?.metadata !== undefined &&
            !feedback_source.metadata["__run"]) {
            feedback_source.metadata["__run"] = { run_id: sourceRunId };
        }
        if (feedback_source?.metadata !== undefined &&
            feedback_source.metadata["__run"]?.run_id !== undefined) {
            assertUuid(feedback_source.metadata["__run"].run_id);
        }
        const feedback = {
            id: feedbackId ?? uuid.v7(),
            run_id: runId,
            key,
            score: _formatFeedbackScore(score),
            value,
            correction,
            comment,
            feedback_source: feedback_source,
            comparative_experiment_id: comparativeExperimentId,
            feedbackConfig,
            session_id: sessionId ?? projectId,
            start_time: startTime,
        };
        const body = JSON.stringify(feedback);
        const url = `${this.apiUrl}/feedback`;
        await this.caller.call(async () => {
            const res = await this._fetch(url, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "create feedback", true);
            return res;
        });
        return feedback;
    }
    async updateFeedback(feedbackId, { score, value, correction, comment, }) {
        const feedbackUpdate = {};
        if (score !== undefined && score !== null) {
            feedbackUpdate["score"] = _formatFeedbackScore(score);
        }
        if (value !== undefined && value !== null) {
            feedbackUpdate["value"] = value;
        }
        if (correction !== undefined && correction !== null) {
            feedbackUpdate["correction"] = correction;
        }
        if (comment !== undefined && comment !== null) {
            feedbackUpdate["comment"] = comment;
        }
        assertUuid(feedbackId);
        const body = JSON.stringify(feedbackUpdate);
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/feedback/${feedbackId}`, {
                method: "PATCH",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update feedback", true);
            return res;
        });
    }
    async readFeedback(feedbackId) {
        assertUuid(feedbackId);
        const path = `/feedback/${feedbackId}`;
        const response = await this._get(path);
        return response;
    }
    async deleteFeedback(feedbackId) {
        assertUuid(feedbackId);
        const path = `/feedback/${feedbackId}`;
        await this.caller.call(async () => {
            const res = await this._fetch(this.apiUrl + path, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, `delete ${path}`, true);
            return res;
        });
    }
    async *listFeedback({ runIds, feedbackKeys, feedbackSourceTypes, } = {}) {
        const queryParams = new URLSearchParams();
        if (runIds) {
            for (const runId of runIds) {
                assertUuid(runId);
                queryParams.append("run", runId);
            }
        }
        if (feedbackKeys) {
            for (const key of feedbackKeys) {
                queryParams.append("key", key);
            }
        }
        if (feedbackSourceTypes) {
            for (const type of feedbackSourceTypes) {
                queryParams.append("source", type);
            }
        }
        for await (const feedbacks of this._getPaginated("/feedback", queryParams)) {
            yield* feedbacks;
        }
    }
    /**
     * Creates a presigned feedback token and URL.
     *
     * The token can be used to authorize feedback metrics without
     * needing an API key. This is useful for giving browser-based
     * applications the ability to submit feedback without needing
     * to expose an API key.
     *
     * @param runId The ID of the run.
     * @param feedbackKey The feedback key.
     * @param options Additional options for the token.
     * @param options.expiration The expiration time for the token.
     *
     * @returns A promise that resolves to a FeedbackIngestToken.
     */
    async createPresignedFeedbackToken(runId, feedbackKey, { expiration, feedbackConfig, } = {}) {
        const body = {
            run_id: runId,
            feedback_key: feedbackKey,
            feedback_config: feedbackConfig,
        };
        if (expiration) {
            if (typeof expiration === "string") {
                body["expires_at"] = expiration;
            }
            else if (expiration?.hours || expiration?.minutes || expiration?.days) {
                body["expires_in"] = expiration;
            }
        }
        else {
            body["expires_in"] = {
                hours: 3,
            };
        }
        const serializedBody = JSON.stringify(body);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/feedback/tokens`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: serializedBody,
            });
            await raiseForStatus(res, "create presigned feedback token");
            return res;
        });
        return await response.json();
    }
    async createComparativeExperiment({ name, experimentIds, referenceDatasetId, createdAt, description, metadata, id, }) {
        if (experimentIds.length === 0) {
            throw new Error("At least one experiment is required");
        }
        if (!referenceDatasetId) {
            referenceDatasetId = (await this.readProject({
                projectId: experimentIds[0],
            })).reference_dataset_id;
        }
        if (!referenceDatasetId == null) {
            throw new Error("A reference dataset is required");
        }
        const body = {
            id,
            name,
            experiment_ids: experimentIds,
            reference_dataset_id: referenceDatasetId,
            description,
            created_at: (createdAt ?? new Date())?.toISOString(),
            extra: {},
        };
        if (metadata)
            body.extra["metadata"] = metadata;
        const serializedBody = JSON.stringify(body);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/datasets/comparative`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: serializedBody,
            });
            await raiseForStatus(res, "create comparative experiment");
            return res;
        });
        return response.json();
    }
    /**
     * Retrieves a list of presigned feedback tokens for a given run ID.
     * @param runId The ID of the run.
     * @returns An async iterable of FeedbackIngestToken objects.
     */
    async *listPresignedFeedbackTokens(runId) {
        assertUuid(runId);
        const params = new URLSearchParams({ run_id: runId });
        for await (const tokens of this._getPaginated("/feedback/tokens", params)) {
            yield* tokens;
        }
    }
    _selectEvalResults(results) {
        let results_;
        if ("results" in results) {
            results_ = results.results;
        }
        else if (Array.isArray(results)) {
            results_ = results;
        }
        else {
            results_ = [results];
        }
        return results_;
    }
    async _logEvaluationFeedback(evaluatorResponse, run, sourceInfo) {
        const evalResults = this._selectEvalResults(evaluatorResponse);
        const feedbacks = [];
        for (const res of evalResults) {
            let sourceInfo_ = sourceInfo || {};
            if (res.evaluatorInfo) {
                sourceInfo_ = { ...res.evaluatorInfo, ...sourceInfo_ };
            }
            let runId_ = null;
            if (res.targetRunId) {
                runId_ = res.targetRunId;
            }
            else if (run) {
                runId_ = run.id;
            }
            feedbacks.push(await this.createFeedback(runId_, res.key, {
                score: res.score,
                value: res.value,
                comment: res.comment,
                correction: res.correction,
                sourceInfo: sourceInfo_,
                sourceRunId: res.sourceRunId,
                feedbackConfig: res.feedbackConfig,
                feedbackSourceType: "model",
                sessionId: run?.session_id,
                startTime: run?.start_time,
            }));
        }
        return [evalResults, feedbacks];
    }
    async logEvaluationFeedback(evaluatorResponse, run, sourceInfo) {
        const [results] = await this._logEvaluationFeedback(evaluatorResponse, run, sourceInfo);
        return results;
    }
    /**
     * API for managing feedback configs
     */
    /**
     * Create a feedback configuration on the LangSmith API.
     *
     * This upserts: if an identical config already exists, it returns it.
     * If a conflicting config exists for the same key, a 400 error is raised.
     *
     * @param options - The options for creating a feedback config
     * @param options.feedbackKey - The unique key for this feedback config
     * @param options.feedbackConfig - The config specifying type, bounds, and categories
     * @param options.isLowerScoreBetter - Whether a lower score is better
     * @returns The created FeedbackConfigSchema object
     */
    async createFeedbackConfig(options) {
        const { feedbackKey, feedbackConfig, isLowerScoreBetter = false } = options;
        const body = {
            feedback_key: feedbackKey,
            feedback_config: feedbackConfig,
            is_lower_score_better: isLowerScoreBetter,
        };
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/feedback-configs`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: JSON.stringify(body),
            });
            await raiseForStatus(res, "create feedback config");
            return res;
        });
        return response.json();
    }
    /**
     * List feedback configurations on the LangSmith API.
     * @param options - The options for listing feedback configs
     * @param options.feedbackKeys - Filter by specific feedback keys
     * @param options.nameContains - Filter by name substring
     * @param options.limit - The maximum number of configs to return
     * @returns An async iterator of FeedbackConfigSchema objects
     */
    async *listFeedbackConfigs(options = {}) {
        const { feedbackKeys, nameContains, limit } = options;
        const params = new URLSearchParams();
        if (feedbackKeys) {
            feedbackKeys.forEach((key) => {
                params.append("key", key);
            });
        }
        if (nameContains)
            params.append("name_contains", nameContains);
        params.append("limit", (limit !== undefined ? Math.min(limit, 100) : 100).toString());
        let count = 0;
        for await (const configs of this._getPaginated("/feedback-configs", params)) {
            yield* configs;
            count += configs.length;
            if (limit !== undefined && count >= limit)
                break;
        }
    }
    /**
     * Update a feedback configuration on the LangSmith API.
     * @param feedbackKey - The key of the feedback config to update
     * @param options - The options for updating the feedback config
     * @param options.feedbackConfig - The new feedback config
     * @param options.isLowerScoreBetter - Whether a lower score is better
     * @returns The updated FeedbackConfigSchema object
     */
    async updateFeedbackConfig(feedbackKey, options = {}) {
        const { feedbackConfig, isLowerScoreBetter } = options;
        const body = { feedback_key: feedbackKey };
        if (feedbackConfig !== undefined) {
            body.feedback_config = feedbackConfig;
        }
        if (isLowerScoreBetter !== undefined) {
            body.is_lower_score_better = isLowerScoreBetter;
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/feedback-configs`, {
                method: "PATCH",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: JSON.stringify(body),
            });
            await raiseForStatus(res, "update feedback config");
            return res;
        });
        return response.json();
    }
    /**
     * Delete a feedback configuration on the LangSmith API.
     * @param feedbackKey - The key of the feedback config to delete
     */
    async deleteFeedbackConfig(feedbackKey) {
        const params = new URLSearchParams({ feedback_key: feedbackKey });
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/feedback-configs?${params}`, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "delete feedback config", true);
            return res;
        });
    }
    /**
     * API for managing annotation queues
     */
    /**
     * List the annotation queues on the LangSmith API.
     * @param options - The options for listing annotation queues
     * @param options.queueIds - The IDs of the queues to filter by
     * @param options.name - The name of the queue to filter by
     * @param options.nameContains - The substring that the queue name should contain
     * @param options.limit - The maximum number of queues to return
     * @returns An iterator of AnnotationQueue objects
     */
    async *listAnnotationQueues(options = {}) {
        const { queueIds, name, nameContains, limit } = options;
        const params = new URLSearchParams();
        if (queueIds) {
            queueIds.forEach((id, i) => {
                assertUuid(id, `queueIds[${i}]`);
                params.append("ids", id);
            });
        }
        if (name)
            params.append("name", name);
        if (nameContains)
            params.append("name_contains", nameContains);
        params.append("limit", (limit !== undefined ? Math.min(limit, 100) : 100).toString());
        let count = 0;
        for await (const queues of this._getPaginated("/annotation-queues", params)) {
            yield* queues;
            count++;
            if (limit !== undefined && count >= limit)
                break;
        }
    }
    /**
     * Create an annotation queue on the LangSmith API.
     * @param options - The options for creating an annotation queue
     * @param options.name - The name of the annotation queue
     * @param options.description - The description of the annotation queue
     * @param options.queueId - The ID of the annotation queue
     * @returns The created AnnotationQueue object
     */
    async createAnnotationQueue(options) {
        const { name, description, queueId, rubricInstructions, rubricItems } = options;
        const body = {
            name,
            description,
            id: queueId || uuid.v4(),
            rubric_instructions: rubricInstructions,
            rubric_items: rubricItems,
        };
        const serializedBody = JSON.stringify(Object.fromEntries(Object.entries(body).filter(([_, v]) => v !== undefined)));
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: serializedBody,
            });
            await raiseForStatus(res, "create annotation queue");
            return res;
        });
        return response.json();
    }
    /**
     * Read an annotation queue with the specified queue ID.
     * @param queueId - The ID of the annotation queue to read
     * @returns The AnnotationQueueWithDetails object
     */
    async readAnnotationQueue(queueId) {
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues/${assertUuid(queueId, "queueId")}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "read annotation queue");
            return res;
        });
        return response.json();
    }
    /**
     * Update an annotation queue with the specified queue ID.
     * @param queueId - The ID of the annotation queue to update
     * @param options - The options for updating the annotation queue
     * @param options.name - The new name for the annotation queue
     * @param options.description - The new description for the annotation queue
     */
    async updateAnnotationQueue(queueId, options) {
        const { name, description, rubricInstructions, rubricItems } = options;
        const bodyObj = {};
        if (name !== undefined)
            bodyObj.name = name;
        if (description !== undefined)
            bodyObj.description = description;
        if (rubricInstructions !== undefined)
            bodyObj.rubric_instructions = rubricInstructions;
        if (rubricItems !== undefined)
            bodyObj.rubric_items = rubricItems;
        const body = JSON.stringify(bodyObj);
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues/${assertUuid(queueId, "queueId")}`, {
                method: "PATCH",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update annotation queue", true);
            return res;
        });
    }
    /**
     * Delete an annotation queue with the specified queue ID.
     * @param queueId - The ID of the annotation queue to delete
     */
    async deleteAnnotationQueue(queueId) {
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues/${assertUuid(queueId, "queueId")}`, {
                method: "DELETE",
                headers: { ...this._mergedHeaders, Accept: "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "delete annotation queue", true);
            return res;
        });
    }
    /**
     * Add runs to an annotation queue with the specified queue ID.
     * @param queueId - The ID of the annotation queue
     * @param runIds - The IDs of the runs to be added to the annotation queue
     */
    async addRunsToAnnotationQueue(queueId, runIds) {
        const body = JSON.stringify(runIds.map((id, i) => assertUuid(id, `runIds[${i}]`).toString()));
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues/${assertUuid(queueId, "queueId")}/runs`, {
                method: "POST",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "add runs to annotation queue", true);
            return res;
        });
    }
    /**
     * Get a run from an annotation queue at the specified index.
     * @param queueId - The ID of the annotation queue
     * @param index - The index of the run to retrieve
     * @returns A Promise that resolves to a RunWithAnnotationQueueInfo object
     * @throws {Error} If the run is not found at the given index or for other API-related errors
     */
    async getRunFromAnnotationQueue(queueId, index) {
        const baseUrl = `/annotation-queues/${assertUuid(queueId, "queueId")}/run`;
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}${baseUrl}/${index}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "get run from annotation queue");
            return res;
        });
        const run = await response.json();
        return _normalizeRunTimestamps(run);
    }
    /**
     * Delete a run from an an annotation queue.
     * @param queueId - The ID of the annotation queue to delete the run from
     * @param queueRunId - The ID of the run to delete from the annotation queue
     */
    async deleteRunFromAnnotationQueue(queueId, queueRunId) {
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues/${assertUuid(queueId, "queueId")}/runs/${assertUuid(queueRunId, "queueRunId")}`, {
                method: "DELETE",
                headers: { ...this._mergedHeaders, Accept: "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "delete run from annotation queue", true);
            return res;
        });
    }
    /**
     * Get the size of an annotation queue.
     * @param queueId - The ID of the annotation queue
     */
    async getSizeFromAnnotationQueue(queueId) {
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/annotation-queues/${assertUuid(queueId, "queueId")}/size`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "get size from annotation queue");
            return res;
        });
        return response.json();
    }
    async _currentTenantIsOwner(owner) {
        const settings = await this._getSettings();
        return owner == "-" || settings.tenant_handle === owner;
    }
    async _ownerConflictError(action, owner) {
        const settings = await this._getSettings();
        return new Error(`Cannot ${action} for another tenant.\n
      Current tenant: ${settings.tenant_handle}\n
      Requested tenant: ${owner}`);
    }
    async _getLatestCommitHash(promptOwnerAndName) {
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/commits/${promptOwnerAndName}/?limit=${1}&offset=${0}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "get latest commit hash");
            return res;
        });
        const json = await response.json();
        if (json.commits.length === 0) {
            return undefined;
        }
        return json.commits[0].commit_hash;
    }
    async _likeOrUnlikePrompt(promptIdentifier, like) {
        const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
        const body = JSON.stringify({ like: like });
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/likes/${owner}/${promptName}`, {
                method: "POST",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, `${like ? "like" : "unlike"} prompt`);
            return res;
        });
        return response.json();
    }
    async _getPromptUrl(promptIdentifier) {
        const [owner, promptName, commitHash] = parseHubIdentifier(promptIdentifier);
        if (!(await this._currentTenantIsOwner(owner))) {
            if (commitHash !== "latest") {
                return `${this.getHostUrl()}/hub/${owner}/${promptName}/${commitHash.substring(0, 8)}`;
            }
            else {
                return `${this.getHostUrl()}/hub/${owner}/${promptName}`;
            }
        }
        else {
            const settings = await this._getSettings();
            if (commitHash !== "latest") {
                return `${this.getHostUrl()}/prompts/${promptName}/${commitHash.substring(0, 8)}?organizationId=${settings.id}`;
            }
            else {
                return `${this.getHostUrl()}/prompts/${promptName}?organizationId=${settings.id}`;
            }
        }
    }
    /**
     * Check if a prompt exists.
     * @param promptIdentifier - The identifier of the prompt. Can be in the format:
     *   - "promptName" (for private prompts, owner defaults to "-")
     *   - "owner/promptName" (for prompts with explicit owner)
     * @returns A Promise that resolves to true if the prompt exists, false otherwise
     * @example
     * ```typescript
     * // Check if a prompt exists before creating a commit
     * if (await client.promptExists("my-prompt")) {
     *   await client.createCommit("my-prompt", template);
     * } else {
     *   await client.createPrompt("my-prompt");
     * }
     * ```
     */
    async promptExists(promptIdentifier) {
        const prompt = await this.getPrompt(promptIdentifier);
        return !!prompt;
    }
    /**
     * Like a prompt.
     * @param promptIdentifier - The identifier of the prompt. Can be in the format:
     *   - "promptName" (for private prompts, owner defaults to "-")
     *   - "owner/promptName" (for prompts with explicit owner)
     * @returns A Promise that resolves to the like response containing the updated like count
     * @example
     * ```typescript
     * // Like a prompt
     * const response = await client.likePrompt("owner/useful-prompt");
     * console.log(`Prompt now has ${response.likes} likes`);
     * ```
     */
    async likePrompt(promptIdentifier) {
        return this._likeOrUnlikePrompt(promptIdentifier, true);
    }
    /**
     * Unlike a prompt (remove a previously added like).
     * @param promptIdentifier - The identifier of the prompt. Can be in the format:
     *   - "promptName" (for private prompts, owner defaults to "-")
     *   - "owner/promptName" (for prompts with explicit owner)
     * @returns A Promise that resolves to the like response containing the updated like count
     * @example
     * ```typescript
     * // Unlike a prompt
     * const response = await client.unlikePrompt("owner/useful-prompt");
     * console.log(`Prompt now has ${response.likes} likes`);
     * ```
     */
    async unlikePrompt(promptIdentifier) {
        return this._likeOrUnlikePrompt(promptIdentifier, false);
    }
    /**
     * List all commits for a prompt.
     * @param promptIdentifier - The identifier of the prompt. Can be in the format:
     *   - "promptName" (for private prompts, owner defaults to "-")
     *   - "owner/promptName" (for prompts with explicit owner)
     *   - "promptName:commitHash" (commit hash is ignored, all commits are returned)
     * @returns An async iterable iterator of PromptCommit objects
     * @example
     * ```typescript
     * // List commits for a private prompt
     * for await (const commit of client.listCommits("my-prompt")) {
     *   console.log(commit);
     * }
     *
     * // List commits for a prompt with explicit owner
     * for await (const commit of client.listCommits("owner/my-prompt")) {
     *   console.log(commit);
     * }
     * ```
     */
    async *listCommits(promptIdentifier) {
        const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
        for await (const commits of this._getPaginated(`/commits/${owner}/${promptName}/`, new URLSearchParams(), (res) => res.commits)) {
            yield* commits;
        }
    }
    /**
     * List prompts by filter.
     * @param options - Optional filters for listing prompts
     * @param options.isPublic - Filter by public/private prompts. If undefined, returns all prompts.
     * @param options.isArchived - Filter by archived status. Defaults to false (non-archived prompts only).
     * @param options.sortField - Field to sort by. Defaults to "updated_at".
     * @param options.query - Search query to filter prompts by name or description.
     * @returns An async iterable iterator of Prompt objects
     * @example
     * ```typescript
     * // List all prompts
     * for await (const prompt of client.listPrompts()) {
     *   console.log(prompt);
     * }
     *
     * // List only public prompts
     * for await (const prompt of client.listPrompts({ isPublic: true })) {
     *   console.log(prompt);
     * }
     *
     * // Search for prompts
     * for await (const prompt of client.listPrompts({ query: "translation" })) {
     *   console.log(prompt);
     * }
     * ```
     */
    async *listPrompts(options) {
        const params = new URLSearchParams();
        params.append("sort_field", options?.sortField ?? "updated_at");
        params.append("sort_direction", "desc");
        params.append("is_archived", (!!options?.isArchived).toString());
        if (options?.isPublic !== undefined) {
            params.append("is_public", options.isPublic.toString());
        }
        if (options?.query) {
            params.append("query", options.query);
        }
        for await (const prompts of this._getPaginated("/repos", params, (res) => res.repos)) {
            yield* prompts;
        }
    }
    /**
     * Get a prompt by its identifier.
     * @param promptIdentifier - The identifier of the prompt. Can be in the format:
     *   - "promptName" (for private prompts, owner defaults to "-")
     *   - "owner/promptName" (for prompts with explicit owner)
     *   - "promptName:commitHash" (commit hash is ignored, latest version is returned)
     * @returns A Promise that resolves to the Prompt object, or null if not found
     * @example
     * ```typescript
     * // Get a private prompt
     * const prompt = await client.getPrompt("my-prompt");
     *
     * // Get a public prompt
     * const publicPrompt = await client.getPrompt("owner/public-prompt");
     * ```
     */
    async getPrompt(promptIdentifier) {
        const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/repos/${owner}/${promptName}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            if (res?.status === 404) {
                return null;
            }
            await raiseForStatus(res, "get prompt");
            return res;
        });
        const result = await response?.json();
        if (result?.repo) {
            return result.repo;
        }
        else {
            return null;
        }
    }
    /**
     * Create a new prompt.
     * @param promptIdentifier - The identifier for the new prompt. Can be in the format:
     *   - "promptName" (creates a private prompt)
     *   - "owner/promptName" (creates a prompt under a specific owner, must match your tenant)
     * @param options - Optional configuration for the prompt
     * @param options.description - A description of the prompt
     * @param options.readme - Markdown content for the prompt's README
     * @param options.tags - Array of tags to categorize the prompt
     * @param options.isPublic - Whether the prompt should be public. Requires a LangChain Hub handle.
     * @returns A Promise that resolves to the created Prompt object
     * @throws {Error} If creating a public prompt without a LangChain Hub handle, or if owner doesn't match current tenant
     * @example
     * ```typescript
     * // Create a private prompt
     * const prompt = await client.createPrompt("my-new-prompt", {
     *   description: "A prompt for translations",
     *   tags: ["translation", "language"]
     * });
     *
     * // Create a public prompt
     * const publicPrompt = await client.createPrompt("my-public-prompt", {
     *   description: "A public translation prompt",
     *   isPublic: true
     * });
     * ```
     */
    async createPrompt(promptIdentifier, options) {
        const settings = await this._getSettings();
        if (options?.isPublic && !settings.tenant_handle) {
            throw new Error(`Cannot create a public prompt without first\n
        creating a LangChain Hub handle.
        You can add a handle by creating a public prompt at:\n
        https://smith.langchain.com/prompts`);
        }
        const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
        if (!(await this._currentTenantIsOwner(owner))) {
            throw await this._ownerConflictError("create a prompt", owner);
        }
        const data = {
            repo_handle: promptName,
            ...(options?.description && { description: options.description }),
            ...(options?.readme && { readme: options.readme }),
            ...(options?.tags && { tags: options.tags }),
            is_public: !!options?.isPublic,
        };
        const body = JSON.stringify(data);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/repos/`, {
                method: "POST",
                headers: { ...this._mergedHeaders, "Content-Type": "application/json" },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "create prompt");
            return res;
        });
        const { repo } = await response.json();
        return repo;
    }
    /**
     * Create a new commit for an existing prompt.
     * @param promptIdentifier - The identifier of the prompt. Can be in the format:
     *   - "promptName" (for private prompts, owner defaults to "-")
     *   - "owner/promptName" (for prompts with explicit owner)
     * @param object - The prompt object/manifest to commit (e.g., ChatPromptTemplate, messages array, etc.)
     * @param options - Optional configuration for the commit
     * @param options.parentCommitHash - The parent commit hash. Defaults to "latest" (the most recent commit).
     * @returns A Promise that resolves to the URL of the newly created commit
     * @throws {Error} If the prompt does not exist
     * @example
     * ```typescript
     * import { ChatPromptTemplate } from "@langchain/core/prompts";
     *
     * // Create a commit with a new version of the prompt
     * const template = ChatPromptTemplate.fromMessages([
     *   ["system", "You are a helpful assistant."],
     *   ["human", "{input}"]
     * ]);
     *
     * const commitUrl = await client.createCommit("my-prompt", template);
     * console.log(`Commit created: ${commitUrl}`);
     *
     * // Create a commit based on a specific parent commit
     * const commitUrl2 = await client.createCommit("my-prompt", template, {
     *   parentCommitHash: "abc123def456"
     * });
     * ```
     */
    async createCommit(promptIdentifier, object, options) {
        if (!(await this.promptExists(promptIdentifier))) {
            throw new Error("Prompt does not exist, you must create it first.");
        }
        const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
        const resolvedParentCommitHash = options?.parentCommitHash === "latest" || !options?.parentCommitHash
            ? await this._getLatestCommitHash(`${owner}/${promptName}`)
            : options?.parentCommitHash;
        const payload = {
            manifest: JSON.parse(JSON.stringify(object)),
            parent_commit: resolvedParentCommitHash,
            ...(options?.description !== undefined && {
                description: options.description,
            }),
        };
        const body = JSON.stringify(payload);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/commits/${owner}/${promptName}`, {
                method: "POST",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "create commit");
            return res;
        });
        const result = await response.json();
        return this._getPromptUrl(`${owner}/${promptName}${result.commit_hash ? `:${result.commit_hash}` : ""}`);
    }
    /**
     * Update examples with attachments using multipart form data.
     * @param updates List of ExampleUpdateWithAttachments objects to upsert
     * @returns Promise with the update response
     */
    async updateExamplesMultipart(datasetId, updates = []) {
        return this._updateExamplesMultipart(datasetId, updates);
    }
    async _updateExamplesMultipart(datasetId, updates = []) {
        if (!(await this._getDatasetExamplesMultiPartSupport())) {
            throw new Error("Your LangSmith deployment does not allow using the multipart examples endpoint, please upgrade your deployment to the latest version.");
        }
        const formData = new FormData();
        for (const example of updates) {
            const exampleId = example.id;
            // Prepare the main example body
            const exampleBody = {
                ...(example.metadata && { metadata: example.metadata }),
                ...(example.split && { split: example.split }),
            };
            // Add main example data
            const stringifiedExample = serializePayloadForTracing(exampleBody, `Serializing body for example with id: ${exampleId}`);
            const exampleBlob = new Blob([stringifiedExample], {
                type: "application/json",
            });
            formData.append(exampleId, exampleBlob);
            // Add inputs if present
            if (example.inputs) {
                const stringifiedInputs = serializePayloadForTracing(example.inputs, `Serializing inputs for example with id: ${exampleId}`);
                const inputsBlob = new Blob([stringifiedInputs], {
                    type: "application/json",
                });
                formData.append(`${exampleId}.inputs`, inputsBlob);
            }
            // Add outputs if present
            if (example.outputs) {
                const stringifiedOutputs = serializePayloadForTracing(example.outputs, `Serializing outputs whle updating example with id: ${exampleId}`);
                const outputsBlob = new Blob([stringifiedOutputs], {
                    type: "application/json",
                });
                formData.append(`${exampleId}.outputs`, outputsBlob);
            }
            // Add attachments if present
            if (example.attachments) {
                for (const [name, attachment] of Object.entries(example.attachments)) {
                    let mimeType;
                    let data;
                    if (Array.isArray(attachment)) {
                        [mimeType, data] = attachment;
                    }
                    else {
                        mimeType = attachment.mimeType;
                        data = attachment.data;
                    }
                    const attachmentBlob = new Blob([data], {
                        type: `${mimeType}; length=${data.byteLength}`,
                    });
                    formData.append(`${exampleId}.attachment.${name}`, attachmentBlob);
                }
            }
            if (example.attachments_operations) {
                const stringifiedAttachmentsOperations = serializePayloadForTracing(example.attachments_operations, `Serializing attachments while updating example with id: ${exampleId}`);
                const attachmentsOperationsBlob = new Blob([stringifiedAttachmentsOperations], {
                    type: "application/json",
                });
                formData.append(`${exampleId}.attachments_operations`, attachmentsOperationsBlob);
            }
        }
        const datasetIdToUse = datasetId ?? updates[0]?.dataset_id;
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}${this._getPlatformEndpointPath(`datasets/${datasetIdToUse}/examples`)}`, {
                method: "PATCH",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: formData,
            });
            await raiseForStatus(res, "update examples");
            return res;
        });
        return response.json();
    }
    /**
     * Upload examples with attachments using multipart form data.
     * @param uploads List of ExampleUploadWithAttachments objects to upload
     * @returns Promise with the upload response
     * @deprecated This method is deprecated and will be removed in future LangSmith versions, please use `createExamples` instead
     */
    async uploadExamplesMultipart(datasetId, uploads = []) {
        return this._uploadExamplesMultipart(datasetId, uploads);
    }
    async _uploadExamplesMultipart(datasetId, uploads = []) {
        if (!(await this._getDatasetExamplesMultiPartSupport())) {
            throw new Error("Your LangSmith deployment does not allow using the multipart examples endpoint, please upgrade your deployment to the latest version.");
        }
        const formData = new FormData();
        for (const example of uploads) {
            const exampleId = (example.id ?? uuid.v4()).toString();
            // Prepare the main example body
            const exampleBody = {
                created_at: example.created_at,
                ...(example.metadata && { metadata: example.metadata }),
                ...(example.split && { split: example.split }),
                ...(example.source_run_id && { source_run_id: example.source_run_id }),
                ...(example.use_source_run_io && {
                    use_source_run_io: example.use_source_run_io,
                }),
                ...(example.use_source_run_attachments && {
                    use_source_run_attachments: example.use_source_run_attachments,
                }),
            };
            // Add main example data
            const stringifiedExample = serializePayloadForTracing(exampleBody, `Serializing body for uploaded example with id: ${exampleId}`);
            const exampleBlob = new Blob([stringifiedExample], {
                type: "application/json",
            });
            formData.append(exampleId, exampleBlob);
            // Add inputs if present
            if (example.inputs) {
                const stringifiedInputs = serializePayloadForTracing(example.inputs, `Serializing inputs for uploaded example with id: ${exampleId}`);
                const inputsBlob = new Blob([stringifiedInputs], {
                    type: "application/json",
                });
                formData.append(`${exampleId}.inputs`, inputsBlob);
            }
            // Add outputs if present
            if (example.outputs) {
                const stringifiedOutputs = serializePayloadForTracing(example.outputs, `Serializing outputs for uploaded example with id: ${exampleId}`);
                const outputsBlob = new Blob([stringifiedOutputs], {
                    type: "application/json",
                });
                formData.append(`${exampleId}.outputs`, outputsBlob);
            }
            // Add attachments if present
            if (example.attachments) {
                for (const [name, attachment] of Object.entries(example.attachments)) {
                    let mimeType;
                    let data;
                    if (Array.isArray(attachment)) {
                        [mimeType, data] = attachment;
                    }
                    else {
                        mimeType = attachment.mimeType;
                        data = attachment.data;
                    }
                    const attachmentBlob = new Blob([data], {
                        type: `${mimeType}; length=${data.byteLength}`,
                    });
                    formData.append(`${exampleId}.attachment.${name}`, attachmentBlob);
                }
            }
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}${this._getPlatformEndpointPath(`datasets/${datasetId}/examples`)}`, {
                method: "POST",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: formData,
            });
            await raiseForStatus(res, "upload examples");
            return res;
        });
        return response.json();
    }
    async updatePrompt(promptIdentifier, options) {
        if (!(await this.promptExists(promptIdentifier))) {
            throw new Error("Prompt does not exist, you must create it first.");
        }
        const [owner, promptName] = parseHubIdentifier(promptIdentifier);
        if (!(await this._currentTenantIsOwner(owner))) {
            throw await this._ownerConflictError("update a prompt", owner);
        }
        const payload = {};
        if (options?.description !== undefined)
            payload.description = options.description;
        if (options?.readme !== undefined)
            payload.readme = options.readme;
        if (options?.tags !== undefined)
            payload.tags = options.tags;
        if (options?.isPublic !== undefined)
            payload.is_public = options.isPublic;
        if (options?.isArchived !== undefined)
            payload.is_archived = options.isArchived;
        // Check if payload is empty
        if (Object.keys(payload).length === 0) {
            throw new Error("No valid update options provided");
        }
        const body = JSON.stringify(payload);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/repos/${owner}/${promptName}`, {
                method: "PATCH",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body,
            });
            await raiseForStatus(res, "update prompt");
            return res;
        });
        return response.json();
    }
    async deletePrompt(promptIdentifier) {
        if (!(await this.promptExists(promptIdentifier))) {
            throw new Error("Prompt does not exist, you must create it first.");
        }
        const [owner, promptName, _] = parseHubIdentifier(promptIdentifier);
        if (!(await this._currentTenantIsOwner(owner))) {
            throw await this._ownerConflictError("delete a prompt", owner);
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/repos/${owner}/${promptName}`, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "delete prompt");
            return res;
        });
        return response.json();
    }
    /**
     * Generate a cache key for a prompt.
     * Format: "{identifier}" or "{identifier}:with_model"
     */
    _getPromptCacheKey(promptIdentifier, includeModel) {
        const suffix = includeModel ? ":with_model" : "";
        return `${promptIdentifier}${suffix}`;
    }
    /**
     * Fetch a prompt commit directly from the API (bypassing cache).
     */
    async _fetchPromptFromApi(promptIdentifier, options) {
        const [owner, promptName, commitHash] = parseHubIdentifier(promptIdentifier);
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/commits/${owner}/${promptName}/${commitHash}${options?.includeModel ? "?include_model=true" : ""}`, {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "pull prompt commit");
            return res;
        });
        const result = await response.json();
        return {
            owner,
            repo: promptName,
            commit_hash: result.commit_hash,
            manifest: result.manifest,
            examples: result.examples,
            hub_model_config: result.model_config,
            hub_model_provider: result.model_provider,
        };
    }
    async pullPromptCommit(promptIdentifier, options) {
        // Check cache first if not skipped
        const refreshFunc = this._fetchPromptFromApi.bind(this, promptIdentifier, options);
        if (!options?.skipCache && this._promptCache) {
            const cacheKey = this._getPromptCacheKey(promptIdentifier, options?.includeModel);
            const cached = this._promptCache.get(cacheKey, refreshFunc);
            if (cached) {
                return cached;
            }
            // Cache miss - fetch from API and cache it
            const result = await refreshFunc();
            this._promptCache.set(cacheKey, result, refreshFunc);
            return result;
        }
        // No cache or skip cache - fetch directly
        return this._fetchPromptFromApi(promptIdentifier, options);
    }
    /**
     * This method should not be used directly, use `import { pull } from "langchain/hub"` instead.
     * Using this method directly returns the JSON string of the prompt rather than a LangChain object.
     * @private
     */
    async _pullPrompt(promptIdentifier, options) {
        const promptObject = await this.pullPromptCommit(promptIdentifier, {
            includeModel: options?.includeModel,
            skipCache: options?.skipCache,
        });
        const prompt = JSON.stringify(promptObject.manifest);
        return prompt;
    }
    async pushPrompt(promptIdentifier, options) {
        // Create or update prompt metadata
        if (await this.promptExists(promptIdentifier)) {
            if (options && Object.keys(options).some((key) => key !== "object")) {
                await this.updatePrompt(promptIdentifier, {
                    description: options?.description,
                    readme: options?.readme,
                    tags: options?.tags,
                    isPublic: options?.isPublic,
                });
            }
        }
        else {
            await this.createPrompt(promptIdentifier, {
                description: options?.description,
                readme: options?.readme,
                tags: options?.tags,
                isPublic: options?.isPublic,
            });
        }
        if (!options?.object) {
            return await this._getPromptUrl(promptIdentifier);
        }
        // Create a commit with the new manifest
        const url = await this.createCommit(promptIdentifier, options?.object, {
            parentCommitHash: options?.parentCommitHash,
            description: options?.commitDescription,
        });
        return url;
    }
    /**
     * Check if an agent repo exists.
     */
    async agentExists(identifier) {
        const [owner, name] = parseHubIdentifier(identifier);
        return this._repoExists(owner, name);
    }
    /**
     * Check if a skill repo exists.
     */
    async skillExists(identifier) {
        const [owner, name] = parseHubIdentifier(identifier);
        return this._repoExists(owner, name);
    }
    /**
     * Pull an agent directory from Hub.
     * @param identifier The identifier (owner/name[:version]).
     * @param options.version Commit hash or tag; overrides identifier's version.
     */
    async pullAgent(identifier, options) {
        return (await this._pullDirectory(identifier, "agent", options?.version));
    }
    /**
     * Pull a skill directory from Hub.
     */
    async pullSkill(identifier, options) {
        return (await this._pullDirectory(identifier, "skill", options?.version));
    }
    /**
     * Push an agent to Hub. Creates the repo if missing, patches metadata if
     * provided, then commits the given files.
     * @returns The URL of the resulting commit.
     */
    async pushAgent(identifier, options) {
        return this._pushDirectory(identifier, "agent", options);
    }
    /**
     * Push a skill to Hub.
     */
    async pushSkill(identifier, options) {
        return this._pushDirectory(identifier, "skill", options);
    }
    /**
     * Delete an agent and all its owned child file repos.
     */
    async deleteAgent(identifier) {
        return this._deleteDirectory(identifier);
    }
    /**
     * Delete a skill and all its owned child file repos.
     */
    async deleteSkill(identifier) {
        return this._deleteDirectory(identifier);
    }
    /**
     * List agent repos. Yields one at a time, auto-paginating.
     */
    async *listAgents(options) {
        yield* this._listReposByType("agent", options);
    }
    /**
     * List skill repos. Yields one at a time, auto-paginating.
     */
    async *listSkills(options) {
        yield* this._listReposByType("skill", options);
    }
    async *_listReposByType(repoType, options) {
        const params = new URLSearchParams();
        params.append("repo_type", repoType);
        params.append("is_archived", (!!options?.isArchived).toString());
        if (options?.isPublic !== undefined) {
            params.append("is_public", options.isPublic.toString());
        }
        if (options?.query) {
            params.append("query", options.query);
        }
        for await (const repos of this._getPaginated("/repos", params, (res) => res.repos)) {
            yield* repos;
        }
    }
    async _pullDirectory(identifier, repoType, version) {
        const [owner, name, parsedVersion] = parseHubIdentifier(identifier);
        const resolvedVersion = version ?? (parsedVersion !== "latest" ? parsedVersion : undefined);
        const url = new URL(`${this.apiUrl}/v1/platform/hub/repos/${owner}/${name}/directories`);
        url.searchParams.set("repo_type", repoType);
        if (resolvedVersion) {
            url.searchParams.set("commit", resolvedVersion);
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(url.toString(), {
                method: "GET",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "pull directory");
            return res;
        });
        return (await response.json());
    }
    async _pushDirectory(identifier, repoType, options) {
        if (options.parentCommit !== undefined &&
            (options.parentCommit.length < 8 || options.parentCommit.length > 64)) {
            throw new Error("parent_commit must be 8-64 characters");
        }
        const [owner, name] = parseHubIdentifier(identifier);
        if (!(await this._currentTenantIsOwner(owner))) {
            throw await this._ownerConflictError(`push ${repoType}`, owner);
        }
        if (await this._repoExists(owner, name)) {
            if (options.description !== undefined ||
                options.readme !== undefined ||
                options.tags !== undefined ||
                options.isPublic !== undefined) {
                await this._updateRepoMetadata(owner, name, options);
            }
        }
        else {
            const REPO_HANDLE_PATTERN = /^[a-z][a-z0-9-_]*$/;
            if (!REPO_HANDLE_PATTERN.test(name)) {
                throw new Error(`Invalid repo_handle ${JSON.stringify(name)}: must match ${REPO_HANDLE_PATTERN}`);
            }
            await this._createRepo(name, repoType, options);
        }
        const body = { files: options.files };
        if (options.parentCommit) {
            body.parent_commit = options.parentCommit;
        }
        const response = await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/v1/platform/hub/repos/${owner}/${name}/directories/commits`, {
                method: "POST",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: JSON.stringify(body),
            });
            await raiseForStatus(res, `push ${repoType}`);
            return res;
        });
        const data = (await response.json());
        const commitHash = data.commit.commit_hash;
        return `${this.getHostUrl()}/hub/${owner}/${name}:${commitHash.slice(0, 8)}`;
    }
    async _deleteDirectory(identifier) {
        const [owner, name] = parseHubIdentifier(identifier);
        if (!(await this._currentTenantIsOwner(owner))) {
            throw await this._ownerConflictError("delete", owner);
        }
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/v1/platform/hub/repos/${owner}/${name}/directories`, {
                method: "DELETE",
                headers: this._mergedHeaders,
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
            });
            await raiseForStatus(res, "delete directory");
            return res;
        });
    }
    async _repoExists(owner, name) {
        try {
            await this.caller.call(async () => {
                const res = await this._fetch(`${this.apiUrl}/repos/${owner}/${name}`, {
                    method: "GET",
                    headers: this._mergedHeaders,
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                });
                await raiseForStatus(res, "check repo exists");
                return res;
            });
            return true;
        }
        catch (e) {
            if (isLangSmithNotFoundError(e)) {
                return false;
            }
            throw e;
        }
    }
    async _createRepo(name, repoType, options) {
        const body = {
            repo_handle: name,
            repo_type: repoType,
            is_public: !!options.isPublic,
        };
        if (options.description !== undefined)
            body.description = options.description;
        if (options.readme !== undefined)
            body.readme = options.readme;
        if (options.tags !== undefined)
            body.tags = options.tags;
        try {
            await this.caller.call(async () => {
                const res = await this._fetch(`${this.apiUrl}/repos/`, {
                    method: "POST",
                    headers: {
                        ...this._mergedHeaders,
                        "Content-Type": "application/json",
                    },
                    signal: AbortSignal.timeout(this.timeout_ms),
                    ...this.fetchOptions,
                    body: JSON.stringify(body),
                });
                await raiseForStatus(res, `create ${repoType}`);
                return res;
            });
        }
        catch (e) {
            if (isLangSmithConflictError(e)) {
                return;
            }
            throw e;
        }
    }
    async _updateRepoMetadata(owner, name, options) {
        const body = {};
        if (options.description !== undefined)
            body.description = options.description;
        if (options.readme !== undefined)
            body.readme = options.readme;
        if (options.tags !== undefined)
            body.tags = options.tags;
        if (options.isPublic !== undefined)
            body.is_public = options.isPublic;
        if (Object.keys(body).length === 0)
            return;
        await this.caller.call(async () => {
            const res = await this._fetch(`${this.apiUrl}/repos/${owner}/${name}`, {
                method: "PATCH",
                headers: {
                    ...this._mergedHeaders,
                    "Content-Type": "application/json",
                },
                signal: AbortSignal.timeout(this.timeout_ms),
                ...this.fetchOptions,
                body: JSON.stringify(body),
            });
            await raiseForStatus(res, "update repo metadata");
            return res;
        });
    }
    /**
     * Clone a public dataset to your own langsmith tenant.
     * This operation is idempotent. If you already have a dataset with the given name,
     * this function will do nothing.
  
     * @param {string} tokenOrUrl The token of the public dataset to clone.
     * @param {Object} [options] Additional options for cloning the dataset.
     * @param {string} [options.sourceApiUrl] The URL of the langsmith server where the data is hosted. Defaults to the API URL of your current client.
     * @param {string} [options.datasetName] The name of the dataset to create in your tenant. Defaults to the name of the public dataset.
     * @returns {Promise<void>}
     */
    async clonePublicDataset(tokenOrUrl, options = {}) {
        const { sourceApiUrl = this.apiUrl, datasetName } = options;
        const [parsedApiUrl, tokenUuid] = this.parseTokenOrUrl(tokenOrUrl, sourceApiUrl);
        const sourceClient = new Client({
            apiUrl: parsedApiUrl,
            // Placeholder API key not needed anymore in most cases, but
            // some private deployments may have API key-based rate limiting
            // that would cause this to fail if we provide no value.
            apiKey: "placeholder",
        });
        const ds = await sourceClient.readSharedDataset(tokenUuid);
        const finalDatasetName = datasetName || ds.name;
        try {
            if (await this.hasDataset({ datasetId: finalDatasetName })) {
                console.log(`Dataset ${finalDatasetName} already exists in your tenant. Skipping.`);
                return;
            }
        }
        catch (_) {
            // `.hasDataset` will throw an error if the dataset does not exist.
            // no-op in that case
        }
        // Fetch examples first, then create the dataset
        const examples = await sourceClient.listSharedExamples(tokenUuid);
        const dataset = await this.createDataset(finalDatasetName, {
            description: ds.description,
            dataType: ds.data_type || "kv",
            inputsSchema: ds.inputs_schema_definition ?? undefined,
            outputsSchema: ds.outputs_schema_definition ?? undefined,
        });
        try {
            await this.createExamples({
                inputs: examples.map((e) => e.inputs),
                outputs: examples.flatMap((e) => (e.outputs ? [e.outputs] : [])),
                datasetId: dataset.id,
            });
        }
        catch (e) {
            console.error(`An error occurred while creating dataset ${finalDatasetName}. ` +
                "You should delete it manually.");
            throw e;
        }
    }
    parseTokenOrUrl(urlOrToken, apiUrl, numParts = 2, kind = "dataset") {
        // Try parsing as UUID
        try {
            assertUuid(urlOrToken); // Will throw if it's not a UUID.
            return [apiUrl, urlOrToken];
        }
        catch (_) {
            // no-op if it's not a uuid
        }
        // Parse as URL
        try {
            const parsedUrl = new URL(urlOrToken);
            const pathParts = parsedUrl.pathname
                .split("/")
                .filter((part) => part !== "");
            if (pathParts.length >= numParts) {
                const tokenUuid = pathParts[pathParts.length - numParts];
                return [apiUrl, tokenUuid];
            }
            else {
                throw new Error(`Invalid public ${kind} URL: ${urlOrToken}`);
            }
        }
        catch (_error) {
            throw new Error(`Invalid public ${kind} URL or token: ${urlOrToken}`);
        }
    }
    /**
     * Cleanup resources held by the client.
     * Stops the cache's background refresh timer.
     */
    cleanup() {
        if (this._promptCache) {
            this._promptCache.stop();
        }
    }
    /**
     * Awaits all pending trace batches. Useful for environments where
     * you need to be sure that all tracing requests finish before execution ends,
     * such as serverless environments.
     *
     * @example
     * ```
     * import { Client } from "langsmith";
     *
     * const client = new Client();
     *
     * try {
     *   // Tracing happens here
     *   ...
     * } finally {
     *   await client.awaitPendingTraceBatches();
     * }
     * ```
     *
     * @returns A promise that resolves once all currently pending traces have sent.
     */
    async awaitPendingTraceBatches() {
        if (this.manualFlushMode) {
            console.warn("[WARNING]: When tracing in manual flush mode, you must call `await client.flush()` manually to submit trace batches.");
            return Promise.resolve();
        }
        /**
         * traceables use a backgrounded promise before updating runs to avoid blocking
         * and to allow waiting for child runs to end. Waiting a small amount of time
         * here ensures that they are able to enqueue their run operation before we await
         * queued run operations below:
         *
         * ```ts
         * const run = await traceable(async () => {
         *   return "Hello, world!";
         * }, { client })();
         *
         * await client.awaitPendingTraceBatches();
         * ```
         */
        await new Promise((resolve) => setTimeout(resolve, 1));
        // Wait for any in-flight drains (whose serialize work may still be
        // in-progress on a worker thread and not yet registered with the
        // batchIngestCaller queue) before checking queue idleness.
        while (this._pendingDrains.size > 0) {
            await Promise.all([...this._pendingDrains]);
        }
        await Promise.all([
            ...this.autoBatchQueue.items.map(({ itemPromise }) => itemPromise),
            this.batchIngestCaller.queue.onIdle(),
        ]);
        if (this.langSmithToOTELTranslator !== undefined) {
            await getDefaultOTLPTracerComponents()?.DEFAULT_LANGSMITH_SPAN_PROCESSOR?.forceFlush();
        }
    }
    /**
     * Returns a string representation of the Client instance.
     * This method is called when the object is converted to a string
     * or logged, ensuring sensitive information like API keys is not exposed.
     *
     * @returns A string representation of the Client.
     */
    toString() {
        const params = [`apiUrl=${JSON.stringify(this.apiUrl)}`];
        if (this.webUrl !== undefined) {
            params.push(`webUrl=${JSON.stringify(this.webUrl)}`);
        }
        if (this.workspaceId !== undefined) {
            params.push(`workspaceId=${JSON.stringify(this.workspaceId)}`);
        }
        return `[LangSmithClient ${params.join(" ")}]`;
    }
    /**
     * Custom inspect method for Node.js.
     * This method is called when the object is inspected in the Node.js REPL
     * or with console.log, ensuring sensitive information like API keys is not exposed.
     *
     * @returns A string representation of the Client for inspection.
     */
    [Symbol.for("nodejs.util.inspect.custom")]() {
        return this.toString();
    }
}
Object.defineProperty(Client, "_fallbackDirsCreated", {
    enumerable: true,
    configurable: true,
    writable: true,
    value: new Set()
});
function isExampleCreate(input) {
    return "dataset_id" in input || "dataset_name" in input;
}
