{"version":3,"file":"llm.mjs","sources":["../../src/llm.ts"],"sourcesContent":["/**\n * LLM API client.\n *\n * This module contains functions used to make requests to the LLM provider API via\n * the Grafana LLM app plugin. That plugin must be installed, enabled and configured\n * in order for these functions to work.\n *\n * The {@link enabled} function can be used to check if the plugin is enabled and configured.\n */\n\nimport {\n  isLiveChannelMessageEvent,\n  LiveChannelMessageEvent,\n} from \"@grafana/data\";\nimport {\n  getBackendSrv,\n  getGrafanaLiveSrv,\n  logDebug /* logError */,\n} from \"@grafana/runtime\";\n\nimport React, { useEffect, useCallback, useState } from \"react\";\nimport { useAsync } from \"react-use\";\nimport { pipe, Observable, UnaryFunction, Subscription } from \"rxjs\";\nimport { filter, map, scan, takeWhile, tap, toArray } from \"rxjs/operators\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport {\n  LLM_PLUGIN_ROUTE,\n  pluginLiveChannel,\n  setLLMPluginVersion,\n} from \"./constants\";\nimport { HealthCheckResponse, LLMProviderHealthDetails } from \"./types\";\n\nconst LLM_CHAT_COMPLETIONS_PATH = \"llm/v1/chat/completions\";\n\n/** The role of a message's author. */\nexport type Role = \"system\" | \"user\" | \"assistant\" | \"function\" | \"tool\";\n\n/** A message in a conversation. */\nexport interface Message {\n  /** The role of the message's author. */\n  role: Role;\n\n  /** The contents of the message. content is required for all messages, and may be null for assistant messages with function calls. */\n  content?: string;\n\n  /** The ID of the tool call, if this message is a function call. */\n  tool_call_id?: string;\n\n  /**\n   * The name of the author of this message.\n   *\n   * This is required if role is 'function', and it should be the name of the function whose response is in the content.\n   *\n   * May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.\n   */\n  name?: string;\n\n  /**\n   * The name and arguments of a function that should be called, as generated by the model.\n   *\n   * @deprecated Use tool_calls instead.\n   */\n  function_call?: Object;\n\n  /**\n   * The tool calls generated by the model, such as function calls.\n   */\n  tool_calls?: ToolCall[];\n}\n\n/** A tool call the model may generate. */\nexport interface ToolCall {\n  id: string;\n  index?: number;\n  type: \"function\";\n  function: FunctionCall;\n}\n\n/** A function call generated by the model. */\ninterface FunctionCall {\n  /**\n   * The name of the tool to call.\n   */\n  name: string;\n\n  /**\n   * The arguments to call the function with, as generated by the model in JSON format.\n   *\n   * Note that the model does not always generate valid JSON, and may hallucinate\n   * parameters not defined by your function schema. Validate the arguments in\n   * your code before calling your function.\n   */\n  arguments: string;\n}\n\n/** A function the model may generate JSON inputs for. */\nexport interface Function {\n  /**\n   * The name of the function to be called.\n   *\n   * Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.\n   */\n  name: string;\n  /**\n   * A description of what the function does, used by the model to choose when and how to call the function.\n   */\n  description?: string;\n  /*\n   * The parameters the functions accepts, described as a JSON Schema object. See the provider's guide for examples, and the JSON Schema reference for documentation about the format.\n   *\n   * Omitting `parameters` defines a function with an empty parameter list.\n   */\n  parameters?: Object;\n  /**\n   * Whether to enable strict schema adherence when generating the function call.\n   *\n   * If set to true, the model will follow the exact schema defined in the parameters field.\n   * Only a subset of JSON Schema is supported when strict is true.\n   */\n  strict?: boolean;\n}\n\n/**\n * Enum representing abstracted models used by the backend app.\n * @enum {string}\n */\nexport enum Model {\n  BASE = \"base\",\n  LARGE = \"large\",\n}\n\n/**\n * @deprecated Use {@link Model} instead.\n */\ntype DeprecatedString = string;\n\nexport interface ChatCompletionsRequest {\n  /**\n   * Model abstraction to use. These abstractions are then translated back into specific models based on the users settings.\n   *\n   * If not specified, defaults to `Model.BASE`.\n   */\n  model?: Model | DeprecatedString;\n  /** A list of messages comprising the conversation so far. */\n  messages: Message[];\n  /**\n   * What sampling temperature to use, between 0 and 2.\n   * Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n   *\n   * We generally recommend altering this or top_p but not both.\n   */\n  temperature?: number;\n  /**\n   * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass.\n   * So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n   *\n   * We generally recommend altering this or temperature but not both.\n   */\n  top_p?: number;\n  /**\n   * How many chat completion choices to generate for each input message.\n   */\n  n?: number;\n  /**\n   * Up to 4 sequences where the API will stop generating further tokens.\n   */\n  stop?: string | string[];\n  /**\n   * The maximum number of tokens to generate in the chat completion.\n   *\n   * This value is now deprecated in favor of `max_completion_tokens`.\n   */\n  max_tokens?: number;\n  /**\n   * An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning tokens.\n   */\n  max_completion_tokens?: number;\n  /**\n   * Number between -2.0 and 2.0.\n   *\n   * Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.\n   */\n  presence_penalty?: number;\n  /**\n   * Number between -2.0 and 2.0.\n   *\n   * Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.\n   */\n  frequency_penalty?: number;\n  /**\n   * Modify the likelihood of specified tokens appearing in the completion.\n   *\n   * Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100.\n   * Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model,\n   * but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban\n   * or exclusive selection of the relevant token.\n   */\n  logit_bias?: { [key: string]: number };\n  /**\n   * A unique identifier representing your end-user, which can help monitor and detect abuse.\n   */\n  user?: string;\n\n  /** A list of tools that the model may use. */\n  tools?: Tool[];\n}\n\n/** A tool that the model may use. */\nexport interface Tool {\n  type: \"function\";\n  /** The function that the model may use. */\n  function: Function;\n}\n\n/** A completion object from the LLM provider. */\nexport interface Choice {\n  /** The message object generated by the model. */\n  message: Message;\n  /**\n   * The reason the model stopped generating text.\n   *\n   * This may be one of:\n   *  - stop: API returned complete message, or a message terminated by one of the stop sequences provided via the stop parameter\n   *  - length: incomplete model output due to max_tokens parameter or token limit\n   *  - function_call: the model decided to call a function\n   *  - content_filter: omitted content due to a flag from our content filters\n   *  - null: API response still in progress or incomplete\n   */\n  finish_reason: string;\n  /** The index of the completion in the list of choices. */\n  index: number;\n}\n\n/** The usage statistics for a request to the LLM provider. */\nexport interface Usage {\n  /** The number of tokens in the prompt. */\n  prompt_tokens: number;\n  /** The number of tokens in the completion. */\n  completion_tokens: number;\n  /** The total number of tokens. */\n  total_tokens: number;\n}\n\n/** The error response from the Grafana LLM app when trying to call the chat completions API. */\ninterface ChatCompletionsErrorResponse {\n  /** The error message. */\n  error: string;\n}\n\n/** A response from the LLM provider Chat Completions API. */\nexport interface ChatCompletionsResponse<T = Choice> {\n  /** The ID of the request. */\n  id: string;\n  /** The type of object returned (e.g. 'chat.completion'). */\n  object: string;\n  /** The timestamp of the request, as a UNIX timestamp. */\n  created: number;\n  /** The name of the model used to generate the response. */\n  model: string;\n  /** A list of completion objects (only one, unless `n > 1` in the request). */\n  choices: T[];\n  /** The number of tokens used to generate the replies, counting prompt, completion, and total. */\n  usage: Usage;\n}\n\n/** A content message returned from the model. */\nexport interface ContentMessage {\n  /** The content of the message. */\n  content: string;\n  /** The role of the author of this message. */\n  role: Role;\n}\n\n/** A message returned from the model indicating that it is done. */\nexport interface DoneMessage {\n  done: boolean;\n}\n\n/** A function call message returned from the model. */\nexport interface FunctionCallMessage {\n  /** The name of the function to call. */\n  name: string;\n  /** The arguments to the function call. */\n  arguments: any[];\n}\n\n/** A tool calls message returned from the model. */\nexport interface ToolCallsMessage {\n  /** The tool calls generated by the model. */\n  tool_calls: ToolCall[];\n  /** The role of the author of this message. */\n  role: Role;\n}\n\n/**\n * A delta returned from a stream of chat completion responses.\n *\n * In practice this will be either a content message or a function call;\n * done messages are filtered out by the `streamChatCompletions` function.\n */\nexport type ChatCompletionsDelta =\n  | ContentMessage\n  | FunctionCallMessage\n  | DoneMessage\n  | ToolCallsMessage;\n\n/** A chunk included in a chat completion response. */\nexport interface ChatCompletionsChunk {\n  /** The delta since the previous chunk. */\n  delta: ChatCompletionsDelta;\n}\n\n/** Return true if the message is a 'content' message. */\nexport function isContentMessage(\n  message: ChatCompletionsDelta,\n): message is ContentMessage {\n  return \"content\" in message;\n}\n\n/** Return true if the message is a 'done' message. */\nexport function isDoneMessage(\n  message: ChatCompletionsDelta,\n): message is DoneMessage {\n  return \"done\" in message && message.done != null;\n}\n\n/** Return true if the response is an error response. */\nexport function isErrorResponse<T>(\n  response: ChatCompletionsResponse<T> | ChatCompletionsErrorResponse,\n): response is ChatCompletionsErrorResponse {\n  return \"error\" in response;\n}\n\n/** Return true if the message is a function call message. */\nexport function isFunctionCallMessage(\n  message: ChatCompletionsDelta,\n): message is FunctionCallMessage {\n  return \"name\" in message && \"arguments\" in message;\n}\n\n/** Return true if the message is a tool calls message. */\nexport function isToolCallsMessage(\n  message: ChatCompletionsDelta,\n): message is ToolCallsMessage {\n  return \"tool_calls\" in message && message.tool_calls != null;\n}\n\n/**\n * An rxjs operator that extracts the content messages from a stream of chat completion responses.\n *\n * @returns An observable that emits the content messages. Each emission will be a string containing the\n *         token emitted by the model.\n * @example <caption>Example of reading all tokens in a stream.</caption>\n * const stream = streamChatCompletions({ model: Model.BASE, messages: [\n *   { role: 'system', content: 'You are a great bot.' },\n *   { role: 'user', content: 'Hello, bot.' },\n * ]}).pipe(extractContent());\n * stream.subscribe({ next: console.log, error: console.error });\n * // Output:\n * // ['Hello', '? ', 'How ', 'are ', 'you', '?']\n */\nexport function extractContent(): UnaryFunction<\n  Observable<ChatCompletionsResponse<ChatCompletionsChunk>>,\n  Observable<string>\n> {\n  return pipe(\n    filter(\n      (response: ChatCompletionsResponse<ChatCompletionsChunk>) =>\n        response.choices.length > 0 &&\n        isContentMessage(response.choices[0].delta),\n    ),\n    // The type assertion is needed here because the type predicate above doesn't seem to propagate.\n    map(\n      (response: ChatCompletionsResponse<ChatCompletionsChunk>) =>\n        (response.choices[0].delta as ContentMessage).content,\n    ),\n  );\n}\n\n/**\n * An rxjs operator that accumulates the content messages from a stream of chat completion responses.\n *\n * @returns An observable that emits the accumulated content messages. Each emission will be a string containing the\n *         content of all messages received so far.\n * @example\n * const stream = streamChatCompletions({ model: Model.BASE, messages: [\n *   { role: 'system', content: 'You are a great bot.' },\n *   { role: 'user', content: 'Hello, bot.' },\n * ]}).pipe(accumulateContent());\n * stream.subscribe({ next: console.log, error: console.error });\n * // Output:\n * // ['Hello', 'Hello! ', 'Hello! How ', 'Hello! How are ', 'Hello! How are you', 'Hello! How are you?']\n */\nexport function accumulateContent(): UnaryFunction<\n  Observable<ChatCompletionsResponse<ChatCompletionsChunk>>,\n  Observable<string>\n> {\n  return pipe(\n    extractContent(),\n    scan((acc, curr) => acc + curr, \"\"),\n  );\n}\n\n/**\n * Make a request to the chat-completions API via the Grafana LLM plugin proxy.\n */\nexport async function chatCompletions(\n  request: ChatCompletionsRequest,\n): Promise<ChatCompletionsResponse> {\n  const response = await getBackendSrv().post<ChatCompletionsResponse>(\n    `/api/plugins/grafana-llm-app/resources/${LLM_CHAT_COMPLETIONS_PATH}`,\n    request,\n    {\n      headers: { \"Content-Type\": \"application/json\" },\n    },\n  );\n  return response;\n}\n\n/**\n * Make a streaming request to the chat-completions API via the Grafana LLM plugin proxy.\n *\n * A stream of tokens will be returned as an `Observable<string>`. Use the `extractContent` operator to\n * filter the stream to only content messages, or the `accumulateContent` operator to obtain a stream of\n * accumulated content messages.\n *\n * The 'done' message will not be emitted; the stream will simply end when this message is encountered.\n *\n * @example <caption>Example of reading all tokens in a stream.</caption>\n * const stream = streamChatCompletions({ model: Model.BASE, messages: [\n *   { role: 'system', content: 'You are a great bot.' },\n *   { role: 'user', content: 'Hello, bot.' },\n * ]}).pipe(extractContent());\n * stream.subscribe({ next: console.log, error: console.error });\n * // Output:\n * // ['Hello', '? ', 'How ', 'are ', 'you', '?']\n *\n * @example <caption>Example of accumulating tokens in a stream.</caption>\n * const stream = streamChatCompletions({ model: Model.BASE, messages: [\n *   { role: 'system', content: 'You are a great bot.' },\n *   { role: 'user', content: 'Hello, bot.' },\n * ]}).pipe(accumulateContent());\n * stream.subscribe({ next: console.log, error: console.error });\n * // Output:\n * // ['Hello', 'Hello! ', 'Hello! How ', 'Hello! How are ', 'Hello! How are you', 'Hello! How are you?']\n */\nexport function streamChatCompletions(\n  request: ChatCompletionsRequest,\n): Observable<ChatCompletionsResponse<ChatCompletionsChunk>> {\n  const channel = pluginLiveChannel(\n    LLM_CHAT_COMPLETIONS_PATH + \"/\" + uuidv4(),\n    request,\n  );\n  const messages = getGrafanaLiveSrv()\n    .getStream(channel)\n    .pipe(filter((event) => isLiveChannelMessageEvent(event))) as Observable<\n    LiveChannelMessageEvent<ChatCompletionsResponse<ChatCompletionsChunk>>\n  >;\n  return messages.pipe(\n    // Filter out messages that don't have the expected structure\n    filter((event) => {\n      // Skip messages with null choices\n      if (!event.message.choices) {\n        return false;\n      }\n      return true;\n    }),\n    tap((event) => {\n      if (isErrorResponse(event.message)) {\n        throw new Error(event.message.error);\n      }\n    }),\n    // Stop the stream when we get a done message or when the finish_reason is \"stop\"\n    takeWhile((event) => {\n      // If it's an error response, we should continue to let the tap operator handle it\n      if (isErrorResponse(event.message)) {\n        return true;\n      }\n\n      // Check for the explicit done message\n      if (\n        event.message.choices &&\n        event.message.choices[0].delta &&\n        \"done\" in event.message.choices[0].delta &&\n        event.message.choices[0].delta.done === true\n      ) {\n        return false;\n      }\n\n      // Check for finish_reason = \"stop\"\n      if (\n        event.message.choices &&\n        \"finish_reason\" in event.message.choices[0] &&\n        event.message.choices[0].finish_reason === \"stop\"\n      ) {\n        return false;\n      }\n\n      return true;\n    }),\n    map((event) => event.message),\n  );\n}\n\nlet loggedWarning = false;\n\n/** Check if the LLM provider API is enabled via the LLM plugin. */\nexport const health = async (): Promise<LLMProviderHealthDetails> => {\n  // First check if the plugin is enabled.\n  try {\n    const settings = await getBackendSrv().get(\n      `${LLM_PLUGIN_ROUTE}/settings`,\n      undefined,\n      undefined,\n      {\n        showSuccessAlert: false,\n        showErrorAlert: false,\n      },\n    );\n    if (!settings.enabled) {\n      return {\n        configured: false,\n        ok: false,\n        error: \"The Grafana LLM plugin is not enabled.\",\n      };\n    }\n  } catch (e) {\n    logDebug(String(e));\n    logDebug(\n      \"Failed to check if LLM provider is enabled. This is expected if the Grafana LLM plugin is not installed, and the above error can be ignored.\",\n    );\n    loggedWarning = true;\n    return {\n      configured: false,\n      ok: false,\n      error: \"The Grafana LLM plugin is not installed.\",\n    };\n  }\n\n  // Run a health check to see if the LLM provider is configured on the plugin.\n  let response: HealthCheckResponse;\n  try {\n    response = await getBackendSrv().get(\n      `${LLM_PLUGIN_ROUTE}/health`,\n      undefined,\n      undefined,\n      {\n        showSuccessAlert: false,\n        showErrorAlert: false,\n      },\n    );\n  } catch (e) {\n    if (!loggedWarning) {\n      logDebug(String(e));\n      logDebug(\n        \"Failed to check if LLM provider is enabled. This is expected if the Grafana LLM plugin is not installed, and the above error can be ignored.\",\n      );\n      loggedWarning = true;\n    }\n    return {\n      configured: false,\n      ok: false,\n      error: \"The Grafana LLM plugin is not installed.\",\n    };\n  }\n\n  const { details } = response;\n  // Update the version if it's present on the response.\n  if (details?.version !== undefined) {\n    setLLMPluginVersion(details.version);\n  }\n  if (details?.llmProvider === undefined) {\n    return {\n      configured: false,\n      ok: false,\n      error: \"The Grafana LLM plugin is outdated; please update it.\",\n    };\n  }\n  return typeof details.llmProvider === \"boolean\"\n    ? { configured: details.llmProvider, ok: details.llmProvider }\n    : details.llmProvider;\n};\n\nexport const enabled = async (): Promise<boolean> => {\n  const healthDetails = await health();\n  return healthDetails.configured && healthDetails.ok;\n};\n\n/**\n * Enum representing different states for a stream.\n * @enum {string}\n */\nexport enum StreamStatus {\n  IDLE = \"idle\",\n  GENERATING = \"generating\",\n  COMPLETED = \"completed\",\n}\n\n/**\n * A constant representing the timeout value in milliseconds.\n * @type {number}\n */\nexport const TIMEOUT = 60000;\n\n/**\n * A type representing the state of an LLM stream.\n * @typedef {Object} LLMStreamState\n * @property {React.Dispatch<React.SetStateAction<Message[]>} setMessages - A function to set messages.\n * @property {string} reply - The reply associated with the stream.\n * @property {typeof StreamStatus} streamStatus - The current status of the stream.\n * @property {Error|undefined} error - An optional error associated with the stream.\n * @property {{\n *    enabled: boolean|undefined;\n *    stream?: undefined;\n *  }|{\n *    enabled: boolean|undefined;\n *    stream: Subscription;\n *  }|undefined} value - A value that can be an object with 'enabled' and 'stream' properties or undefined.\n */\nexport type LLMStreamState = {\n  setMessages: React.Dispatch<React.SetStateAction<Message[]>>;\n  reply: string;\n  streamStatus: StreamStatus;\n  error: Error | undefined;\n  value:\n    | {\n        enabled: boolean | undefined;\n        stream?: undefined;\n      }\n    | {\n        enabled: boolean | undefined;\n        stream: Subscription;\n      }\n    | undefined;\n};\n\n/**\n * A custom React hook for managing an LLM stream that communicates with the provided model.\n *\n * @param {string} [model=Model.LARGE] - The LLM model to use for communication.\n * @param {number} [temperature=1] - The temperature value for text generation (default is 1).\n * @param {function} [notifyError] - A callback function for handling errors.\n * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds for the initial response before the stream is considered failed.\n *\n * @returns {LLMStreamState} - An object containing the state of the LLM stream.\n * @property {function} setMessages - A function to update the list of messages in the stream.\n * @property {string} reply - The most recent reply received from the LLM stream.\n * @property {StreamStatus} streamStatus - The status of the stream (\"idle\", \"generating\" or \"completed\").\n * @property {Error|undefined} error - An error object if an error occurs, or undefined if no error.\n * @property {object|undefined} value - The current value of the stream.\n * @property {boolean|undefined} value.enabled - Indicates whether the stream is enabled (true or false).\n * @property {Subscription|undefined} value.stream - The stream subscription object if the stream is active, or undefined if not.\n */\nexport function useLLMStream(\n  model = Model.LARGE,\n  temperature = 1,\n  notifyError: (\n    title: string,\n    text?: string,\n    traceId?: string,\n  ) => void = () => {},\n  timeout = TIMEOUT,\n): LLMStreamState {\n  // The messages array to send to the LLM.\n  const [messages, setMessages] = useState<Message[]>([]);\n  // The latest reply from the LLM.\n  const [reply, setReply] = useState(\"\");\n  const [streamStatus, setStreamStatus] = useState<StreamStatus>(\n    StreamStatus.IDLE,\n  );\n  const [error, setError] = useState<Error>();\n\n  const onError = useCallback(\n    (e: Error) => {\n      setStreamStatus(StreamStatus.IDLE);\n      setMessages([]);\n      setError(e);\n      notifyError(\n        \"Failed to generate content using LLM provider\",\n        `Please try again or if the problem persists, contact your organization admin.`,\n      );\n      console.error(e);\n    },\n    [notifyError],\n  );\n\n  const { error: enabledError, value: isEnabled } = useAsync(\n    async () => await enabled(),\n    [enabled],\n  );\n\n  const { error: asyncError, value } = useAsync(async () => {\n    if (!isEnabled || !messages.length) {\n      return { enabled: isEnabled };\n    }\n\n    setStreamStatus(StreamStatus.GENERATING);\n    setError(undefined);\n    // Stream the completions. Each element is the next stream chunk.\n    const stream = streamChatCompletions({\n      model,\n      temperature,\n      messages,\n    }).pipe(\n      // Accumulate the stream content into a stream of strings, where each\n      // element contains the accumulated message so far.\n      accumulateContent(),\n      // The stream is just a regular Observable, so we can use standard rxjs\n      // functionality to update state, e.g. recording when the stream\n      // has completed.\n      // The operator decision tree on the rxjs website is a useful resource:\n      // https://rxjs.dev/operator-decision-tree.)\n    );\n    // Subscribe to the stream and update the state for each returned value.\n    return {\n      enabled: isEnabled,\n      stream: stream.subscribe({\n        next: setReply,\n        error: onError,\n        complete: () => {\n          setStreamStatus(StreamStatus.COMPLETED);\n          setTimeout(() => {\n            setStreamStatus(StreamStatus.IDLE);\n          });\n          setMessages([]);\n          setError(undefined);\n        },\n      }),\n    };\n  }, [messages, isEnabled]);\n\n  // Unsubscribe from the stream when the component unmounts.\n  useEffect(() => {\n    return () => {\n      if (value?.stream) {\n        value.stream.unsubscribe();\n      }\n    };\n  }, [value]);\n\n  // If the stream is generating and we haven't received a reply, it times out.\n  useEffect(() => {\n    let timeout_: NodeJS.Timeout | undefined;\n    if (streamStatus === StreamStatus.GENERATING && reply === \"\") {\n      timeout_ = setTimeout(() => {\n        onError(new Error(`LLM stream timed out after ${timeout}ms`));\n      }, timeout);\n    }\n    return () => {\n      timeout_ && clearTimeout(timeout_);\n    };\n  }, [streamStatus, reply, onError, timeout]);\n\n  if (asyncError || enabledError) {\n    setError(asyncError || enabledError);\n  }\n\n  return {\n    setMessages,\n    reply,\n    streamStatus,\n    error,\n    value,\n  };\n}\n\n/**\n * An rxjs operator that accumulates tool call messages from a stream of chat completion responses into a complete tool call message.\n *\n * @returns An observable that emits the accumulated tool call message when complete.\n * @example\n * const stream = streamChatCompletions({...}).pipe(\n *   accumulateToolCalls()\n * );\n * stream.subscribe({\n *   next: (toolCallMessage) => console.log('Received complete tool call:', toolCallMessage),\n *   error: console.error\n * });\n */\nexport function accumulateToolCalls(): UnaryFunction<\n  Observable<ChatCompletionsResponse<ChatCompletionsChunk>>,\n  Observable<ToolCallsMessage>\n> {\n  return pipe(\n    filter((response: ChatCompletionsResponse<ChatCompletionsChunk>) =>\n      isToolCallsMessage(response.choices[0].delta),\n    ),\n    // Collect all tool call chunks\n    toArray(),\n    // Process the array to reconstruct the complete tool call message\n    map((responses: Array<ChatCompletionsResponse<ChatCompletionsChunk>>) => {\n      const toolCallChunks = responses.map(\n        (r) => r.choices[0].delta as ToolCallsMessage,\n      );\n      return recoverToolCallMessage(toolCallChunks);\n    }),\n  );\n}\n\n/**\n * Recovers a complete tool call message from individual chunks.\n *\n * @param toolCallMessages - Array of tool call message chunks\n * @returns A complete tool call message with all chunks combined\n */\nexport function recoverToolCallMessage(\n  toolCallMessages: ToolCallsMessage[],\n): ToolCallsMessage {\n  const recoveredToolCallMessage: ToolCallsMessage = {\n    role: \"assistant\",\n    tool_calls: [],\n  };\n\n  for (const msg of toolCallMessages) {\n    for (const tc of msg.tool_calls) {\n      if (tc.index! >= recoveredToolCallMessage.tool_calls.length) {\n        recoveredToolCallMessage.tool_calls.push({\n          ...tc,\n          function: { ...tc.function, arguments: tc.function.arguments ?? \"\" },\n        });\n      } else {\n        recoveredToolCallMessage.tool_calls[tc.index!].function.arguments +=\n          tc.function.arguments ?? \"\";\n      }\n    }\n  }\n\n  // Ensure final arguments are never empty\n  for (const tc of recoveredToolCallMessage.tool_calls) {\n    if (!tc.function.arguments) {\n      tc.function.arguments = \"{}\";\n    }\n  }\n\n  return recoveredToolCallMessage;\n}\n"],"names":["Model","uuidv4","loggedWarning","health","enabled","StreamStatus"],"mappings":"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"}