import { EventEmitter } from "node:events";
import { APIConnectionError, APIError } from "../_exceptions.js";
import { log } from "../log.js";
import { recordException, traceTypes, tracer } from "../telemetry/index.js";
import { intervalForRetry } from "../types.js";
import { AsyncIterableQueue, delay, startSoon, toError } from "../utils.js";
import {} from "./chat_context.js";
import {
  toToolContext
} from "./tool_context.js";
class LLM extends EventEmitter {
  constructor() {
    super();
  }
  /**
   * Get the model name/identifier for this LLM instance.
   *
   * @returns The model name if available, "unknown" otherwise.
   *
   * @remarks
   * Plugins should override this property to provide their model information.
   */
  get model() {
    return "unknown";
  }
  /**
   * Get the provider name for this LLM instance.
   *
   * @returns The provider name if available, "unknown" otherwise.
   *
   * @remarks
   * Plugins should override this property to provide their provider information.
   */
  get provider() {
    return "unknown";
  }
  /**
   * Pre-warm connection to the LLM service
   */
  prewarm() {
  }
  async aclose() {
  }
}
class LLMStream {
  output = new AsyncIterableQueue();
  queue = new AsyncIterableQueue();
  closed = false;
  abortController = new AbortController();
  _connOptions;
  logger = log();
  #llm;
  #chatCtx;
  #toolCtx;
  #llmRequestSpan;
  // Provider-known response ids collected from ChatChunks during the current
  // attempt; reset before each retry and written to the `llm_request_run` span.
  #providerRequestIds = [];
  constructor(llm, {
    chatCtx,
    toolCtx,
    connOptions
  }) {
    this.#llm = llm;
    this.#chatCtx = chatCtx;
    this.#toolCtx = toToolContext(toolCtx);
    this._connOptions = connOptions;
    this.monitorMetrics();
    this.abortController.signal.addEventListener("abort", () => {
      this.output.close();
      this.closed = true;
    });
    startSoon(() => this.mainTask().finally(() => this.queue.close()));
  }
  _mainTaskImpl = async (span) => {
    this.#llmRequestSpan = span;
    span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.#llm.model);
    for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
      try {
        return await tracer.startActiveSpan(
          async (attemptSpan) => {
            attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);
            this.#providerRequestIds = [];
            try {
              return await this.run();
            } catch (error) {
              recordException(attemptSpan, toError(error));
              throw error;
            } finally {
              if (this.#providerRequestIds.length) {
                attemptSpan.setAttribute(
                  traceTypes.ATTR_PROVIDER_REQUEST_IDS,
                  this.#providerRequestIds
                );
              }
            }
          },
          { name: "llm_request_run" }
        );
      } catch (error) {
        if (error instanceof APIError) {
          const retryInterval = intervalForRetry(this._connOptions, i);
          if (this._connOptions.maxRetry === 0 || !error.retryable) {
            this.emitError({ error, recoverable: false });
            throw error;
          } else if (i === this._connOptions.maxRetry) {
            this.emitError({ error, recoverable: false });
            throw new APIConnectionError({
              message: `failed to generate LLM completion after ${this._connOptions.maxRetry + 1} attempts`,
              options: { retryable: false }
            });
          } else {
            this.emitError({ error, recoverable: true });
            this.logger.warn(
              { llm: this.#llm.label(), attempt: i + 1, error },
              `failed to generate LLM completion, retrying in ${retryInterval}ms`
            );
          }
          if (retryInterval > 0) {
            await delay(retryInterval);
          }
        } else {
          this.emitError({ error: toError(error), recoverable: false });
          throw error;
        }
      }
    }
  };
  mainTask = async () => tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {
    name: "llm_request",
    endOnExit: false
  });
  emitError({ error, recoverable }) {
    this.#llm.emit("error", {
      type: "llm_error",
      timestamp: Date.now(),
      label: this.#llm.label(),
      error,
      recoverable
    });
  }
  async monitorMetrics() {
    const startTime = process.hrtime.bigint();
    let ttft = BigInt(-1);
    let requestId = "";
    let usage;
    let completionStartTime;
    for await (const ev of this.queue) {
      if (this.abortController.signal.aborted) {
        break;
      }
      this.output.put(ev);
      requestId = ev.id;
      if (requestId && !this.#providerRequestIds.includes(requestId)) {
        this.#providerRequestIds.push(requestId);
      }
      if (ttft === BigInt(-1)) {
        ttft = process.hrtime.bigint() - startTime;
        completionStartTime = (/* @__PURE__ */ new Date()).toISOString();
      }
      if (ev.usage) {
        usage = ev.usage;
      }
    }
    this.output.close();
    const duration = process.hrtime.bigint() - startTime;
    const durationMs = Math.trunc(Number(duration / BigInt(1e6)));
    const metrics = {
      type: "llm_metrics",
      timestamp: Date.now(),
      requestId,
      ttftMs: ttft === BigInt(-1) ? -1 : Math.trunc(Number(ttft / BigInt(1e6))),
      durationMs,
      cancelled: this.abortController.signal.aborted,
      label: this.#llm.label(),
      completionTokens: (usage == null ? void 0 : usage.completionTokens) || 0,
      promptTokens: (usage == null ? void 0 : usage.promptTokens) || 0,
      promptCachedTokens: (usage == null ? void 0 : usage.promptCachedTokens) || 0,
      totalTokens: (usage == null ? void 0 : usage.totalTokens) || 0,
      tokensPerSecond: (() => {
        if (durationMs <= 0) {
          return 0;
        }
        return ((usage == null ? void 0 : usage.completionTokens) || 0) / (durationMs / 1e3);
      })(),
      metadata: {
        modelProvider: this.#llm.provider,
        modelName: this.#llm.model
      }
    };
    if (this.#llmRequestSpan) {
      this.#llmRequestSpan.setAttribute(traceTypes.ATTR_LLM_METRICS, JSON.stringify(metrics));
      this.#llmRequestSpan.setAttributes({
        [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.promptTokens,
        [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.completionTokens
      });
      if (completionStartTime) {
        this.#llmRequestSpan.setAttribute(
          traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME,
          completionStartTime
        );
      }
      this.#llmRequestSpan.end();
    }
    this.#llm.emit("metrics_collected", metrics);
  }
  /** The function context of this stream. */
  get toolCtx() {
    return this.#toolCtx;
  }
  /** The initial chat context of this stream. */
  get chatCtx() {
    return this.#chatCtx;
  }
  /** The connection options for this stream. */
  get connOptions() {
    return this._connOptions;
  }
  next() {
    return this.output.next();
  }
  close() {
    this.abortController.abort();
  }
  /**
   * Collect the entire stream into a single response.
   *
   * @example
   * ```ts
   * const response = await myLlm.chat({ chatCtx, toolCtx }).collect();
   *
   * for (const tc of response.toolCalls) {
   *   // execute the tool call...
   * }
   * ```
   */
  async collect() {
    const textParts = [];
    const toolCalls = [];
    let usage;
    const extra = {};
    try {
      for await (const chunk of this) {
        if (chunk.delta) {
          if (chunk.delta.content) {
            textParts.push(chunk.delta.content);
          }
          if (chunk.delta.toolCalls) {
            toolCalls.push(...chunk.delta.toolCalls);
          }
          if (chunk.delta.extra) {
            Object.assign(extra, chunk.delta.extra);
          }
        }
        if (chunk.usage !== void 0) {
          usage = chunk.usage;
        }
      }
    } finally {
      this.close();
    }
    return {
      text: textParts.join("").trim(),
      toolCalls,
      usage,
      extra
    };
  }
  [Symbol.asyncIterator]() {
    return this;
  }
}
export {
  LLM,
  LLMStream
};
//# sourceMappingURL=llm.js.map