Nothing
      #' @title Make a request to an LLM provider
#'
#' @description This is a helper function which facilitates making requests to LLM
#' providers which follow the structure of the OpenAI API or the Ollama
#' API. It handles both streaming and non-streaming requests.
#'
#' This function is part of the internal API and is not intended to be called directly by
#' users. It is used in some of the pre-built [llm_provider-class] objects
#' included in 'tidyprompt' (e.g., [llm_provider_openai()) and
#' [llm_provider_ollama()]).
#'
#' @param chat_history A data frame with 'role' and 'content' columns
#' (see [chat_history()])
#' @param request A 'httr2' request object with the URL, headers, and body
#' @param stream Logical indicating whether the API should stream responses
#' @param verbose Logical indicating whether interactions should be printed to the console
#' @param api_type API type, one of "openai" or "ollama"
#'
#' @return A list with the completed chat history and the HTTP request and response
#' objects
#'
#' @keywords internal
#' @noRd
request_llm_provider <- function(
  chat_history,
  request,
  stream = NULL,
  verbose = getOption("tidyprompt.verbose", TRUE),
  api_type = c("openai", "ollama")
) {
  api_type <- match.arg(api_type)
  request <- normalize_openai_request(request, api_type)
  if (!is.null(stream) && stream) {
    req_result <- req_llm_stream(request, api_type, verbose)
  } else {
    req_result <- req_llm_non_stream(request, api_type, verbose)
  }
  stopifnot(
    is.list(req_result),
    "new" %in% names(req_result),
    is.data.frame(req_result$new)
  )
  completed <- dplyr::bind_rows(chat_history, req_result$new)
  return(
    list(
      completed = completed,
      http = list(
        request = request,
        response = req_result$httr2_response
      )
    )
  )
}
req_llm_handle_error <- function(e) {
  msg <- paste0(e$message)
  # Try to parse and include JSON body if available
  body_msg <- tryCatch(
    {
      body <- e$resp |>
        httr2::resp_body_string() |>
        jsonlite::fromJSON()
      paste0(
        "\nResponse body: ",
        jsonlite::toJSON(body, pretty = TRUE, auto_unbox = TRUE)
      )
    },
    error = function(e) "\n(Could not parse JSON body from response)"
  )
  msg <- paste0(
    msg,
    body_msg,
    "\nUse 'httr2::last_response()' and 'httr2::last_request()' for more information"
  )
  stop(msg, call. = FALSE)
}
req_llm_stream <- function(req, api_type, verbose) {
  role <- NULL
  message_accumulator <- ""
  tool_calls <- list()
  using_responses <- (api_type == "openai") && is_openai_responses_endpoint(req)
  # No low-level HTTP verbosity; we'll print only tokens ourselves
  resp <- tryCatch(
    httr2::req_perform_connection(req, verbosity = 0),
    error = function(e) req_llm_handle_error(e)
  )
  if (api_type == "ollama") {
    while (!httr2::resp_stream_is_complete(resp)) {
      lines <- httr2::resp_stream_lines(resp, lines = 1, warn = FALSE)
      if (length(lines) == 0) next
      for (line in lines) {
        if (!nzchar(line)) next
        data <- tryCatch(jsonlite::fromJSON(line), error = function(e) NULL)
        if (is.null(data)) next
        if (is.null(role) && !is.null(data$message$role))
          role <- data$message$role
        if (!is.null(data$message$content)) {
          message_accumulator <- paste0(
            message_accumulator,
            data$message$content
          )
          if (isTRUE(verbose)) cat(data$message$content)
        }
      }
    }
    cat("\n")
  } else if (!using_responses) {
    # OpenAI Chat Completions (SSE) — use resp_stream_sse for robust parsing
    if (is.null(role)) role <- "assistant"
    repeat {
      ev <- httr2::resp_stream_sse(resp)
      if (!nzchar(ev$data)) next # keepalive / empty
      if (identical(ev$data, "[DONE]")) {
        cat("\n")
        break
      }
      payload <- tryCatch(jsonlite::fromJSON(ev$data), error = function(e) NULL)
      if (is.null(payload)) next
      # tool calls (function calling) support
      tc <- payload$choices[1, ]$delta$tool_calls
      if (length(tc) > 0) {
        tool_calls <- append_or_update_tool_calls(
          tool_calls,
          tc,
          verbose
        )
        next
      }
      d <- tryCatch(
        payload$choices[1, ]$delta,
        error = function(e) NULL
      )
      if (is.null(d)) next
      if (!is.null(d$role) && is.null(role)) role <- d$role
      # text tokens
      if (!is.null(d$content)) {
        add <- d$content
        if (length(add) > 0 && !is.na(add)) {
          message_accumulator <- paste0(message_accumulator, add)
          if (isTRUE(verbose)) cat(add)
        }
      }
    }
  } else {
    # OpenAI Responses API (SSE)
    if (is.null(role)) role <- "assistant"
    response_id <- NULL
    tool_calls <- list()
    args_buf <- new.env(parent = emptyenv()) # per-tool_call_id argument accumulator
    repeat {
      ev <- httr2::resp_stream_sse(resp)
      if (is.null(ev)) {
        cat("\n")
        break
      }
      if (!nzchar(ev$data) || identical(ev$data, "[DONE]")) next
      payload <- tryCatch(
        jsonlite::fromJSON(ev$data, simplifyVector = FALSE),
        error = function(e) NULL
      )
      if (is.null(payload)) next
      tp <- payload$type %||% ""
      # capture id once
      if (is.null(response_id) && !is.null(payload$id))
        response_id <- payload$id
      # 1) normal text deltas
      if (
        grepl("response\\.(output_)?text\\.delta$", tp) &&
          !is.null(payload$delta)
      ) {
        message_accumulator <- paste0(
          message_accumulator,
          as.character(payload$delta)
        )
        if (isTRUE(verbose)) cat(payload$delta)
        next
      }
      # 2) listen for tool calls
      if (identical(tp, "response.output_item.done")) {
        if (
          !is.null(payload$item) &&
            is.list(payload$item) &&
            identical(payload$item$type, "function_call")
        ) {
          tool_calls <- append_or_update_tool_calls(
            tool_calls,
            list(
              list(
                id = payload$item$id,
                type = "function",
                `function` = list(
                  name = payload$item$name,
                  arguments = payload$item$arguments |>
                    jsonlite::fromJSON() |>
                    jsonlite::toJSON(auto_unbox = TRUE, pretty = TRUE) |>
                    as.character()
                )
              )
            ),
            verbose
          )
          break
        }
        next
      }
    }
  }
  # attach tool_calls and response_id so the caller can act
  if (!is.list(resp$body)) resp$body <- list()
  resp$body$tool_calls <- tool_calls
  if (exists("response_id")) {
    resp$body$response_id <- response_id
  }
  list(
    new = data.frame(
      role = if (is.null(role)) "assistant" else role,
      content = message_accumulator,
      stringsAsFactors = FALSE
    ),
    httr2_response = resp
  )
}
req_llm_non_stream <- function(req, api_type, verbose) {
  response <- tryCatch(
    httr2::req_perform(req),
    error = function(e) req_llm_handle_error(e)
  )
  content <- httr2::resp_body_json(response)
  using_responses <- (api_type == "openai") && is_openai_responses_endpoint(req)
  if (api_type == "ollama") {
    new <- tryCatch(
      data.frame(
        role = content$message$role,
        content = content$message$content,
        stringsAsFactors = FALSE
      ),
      error = function(e) data.frame()
    )
  } else if (using_responses) {
    # Prefer convenience field if present
    txt <- NULL
    if (!is.null(content$output_text)) {
      txt <- content$output_text
    } else if (!is.null(content$output) && length(content$output) > 0) {
      # Fallback: stitch message items' text
      bits <- unlist(lapply(content$output, function(it) {
        if (!is.null(it$content)) {
          unlist(lapply(it$content, function(c) c$text %||% NULL))
        }
      }))
      txt <- paste(bits, collapse = "")
    }
    new <- data.frame(
      role = "assistant",
      content = txt %||% "",
      stringsAsFactors = FALSE
    )
  } else {
    # OpenAI Chat Completions
    new <- tryCatch(
      data.frame(
        role = content$choices[[1]]$message$role,
        content = content$choices[[1]]$message$content,
        stringsAsFactors = FALSE
      ),
      error = function(e) data.frame()
    )
  }
  list(new = new, httr2_response = response)
}
append_or_update_tool_calls <- function(tool_calls, new_tool_calls, verbose) {
  tool_call <- new_tool_calls[[1]]
  id <- tool_call$id
  last_id <- if (length(tool_calls) > 0)
    tool_calls[[length(tool_calls)]]$id else NULL
  if (!is.null(id) & (is.null(last_id) || (id != last_id))) {
    tool_calls <- append(
      tool_calls,
      list(
        list(
          id = tool_call$id,
          type = tool_call$type,
          `function` = list(
            name = tool_call$`function`$name,
            args = tool_call$`function`$arguments
          )
        )
      )
    )
  } else {
    # Update arguments of the last call
    arguments_current <- tool_calls[[length(tool_calls)]]$`function`$arguments
    arguments_new <- tool_call$`function`$arguments
    if (length(arguments_new) > 0) {
      tool_calls[[length(tool_calls)]]$`function`$arguments <- paste0(
        arguments_current,
        arguments_new
      )
    }
  }
  tool_calls
}
is_openai_responses_endpoint <- function(req) {
  url_str <- tryCatch(
    httr2::url_build(req$url),
    error = function(e) as.character(req$url)
  )
  grepl("/responses(\\b|/|\\?)", url_str)
}
normalize_openai_request <- function(req, api_type) {
  if (api_type != "openai") return(req)
  if (!is_openai_responses_endpoint(req)) return(req)
  body_obj <- tryCatch(req$body$data, error = function(e) NULL)
  if (!is.list(body_obj)) return(req)
  # Chat Completions -> Responses rename
  if (!is.null(body_obj$messages)) {
    msgs <- body_obj$messages
    msgs <- lapply(msgs, function(m) {
      m$tool_calls <- NULL
      m$function_call <- NULL
      m$name <- NULL
      if (is.null(m$content)) m$content <- ""
      # Convert CC tool-role messages to typed items for Responses
      if (identical(m$role, "tool") && !is.null(m$tool_call_id)) {
        return(list(
          type = "function_call_output",
          call_id = m$tool_call_id,
          output = as.character(m$content %||% "")
        ))
      }
      m
    })
    needs_fc <- function(item) {
      is.list(item) &&
        isTRUE(identical(item$type, "function_call_output")) &&
        !is.null(item$call_id)
    }
    has_fc_for <- function(call_id, items) {
      any(vapply(
        items,
        function(x)
          is.list(x) &&
            identical(x$type, "function_call") &&
            identical(x$call_id, call_id),
        logical(1)
      ))
    }
    input <- list()
    for (m in msgs) {
      if (needs_fc(m) && !has_fc_for(m$call_id, input)) {
        input[[length(input) + 1]] <- list(
          type = "function_call",
          call_id = m$call_id,
          name = "-",
          arguments = "-"
        )
      }
      input[[length(input) + 1]] <- m
    }
    body_obj$input <- input
    body_obj$messages <- NULL
  }
  # response_format -> text.format  (Responses API)
  if (!is.null(body_obj$response_format) && is.null(body_obj$text$format)) {
    rf <- body_obj$response_format
    if (is.character(rf)) rf <- list(type = rf)
    fmt <- NULL
    if (identical(rf$type, "json_object")) {
      fmt <- list(type = "json_object")
    } else if (identical(rf$type, "json_schema")) {
      js <- rf$json_schema %||% rf$schema %||% rf
      name <- js$name %||% js$title %||% "Schema"
      schema <- js$schema %||% js
      strict <- isTRUE(rf$strict %||% js$strict)
      fmt <- list(
        type = "json_schema",
        name = as.character(name),
        schema = schema
      )
      if (strict) fmt$strict <- TRUE
    }
    if (!is.null(fmt)) {
      body_obj$text <- body_obj$text %||% list()
      body_obj$text$format <- fmt
      body_obj$response_format <- NULL
    }
  }
  # SAFETY GUARD: Only touch list-like items with a role
  if (is.list(body_obj$input)) {
    for (i in seq_along(body_obj$input)) {
      item <- body_obj$input[[i]]
      # skip non-lists (e.g., atomic strings) or items without a role
      if (!(is.list(item) && !is.null(item$role))) next
      # If any lingering tool-role items slipped through, degrade to "system"
      # (You already convert tool messages above; this is just a fallback.)
      if (identical(item$role, "tool")) {
        if (!is.null(item$tool_call_id) && !is.null(item$content)) {
          body_obj$input[[i]] <- list(
            type = "function_call_output",
            call_id = item$tool_call_id,
            output = as.character(item$content %||% "")
          )
        } else {
          body_obj$input[[i]]$role <- "system"
        }
      }
    }
  }
  # Tools normalization for Responses API
  if (!is.null(body_obj$tools) && is.list(body_obj$tools)) {
    body_obj$tools <- lapply(body_obj$tools, function(t) {
      if (is.null(t$type) && !is.null(t$`function`)) t$type <- "function"
      if (is.null(t$name) && !is.null(t$`function`$name))
        t$name <- t$`function`$name
      if (is.null(t$description) && !is.null(t$`function`$description))
        t$description <- t$`function`$description
      if (is.null(t$parameters) && !is.null(t$`function`$parameters))
        t$parameters <- t$`function`$parameters
      t
    })
  }
  # Normalize tool_choice shape
  if (!is.null(body_obj$tool_choice) && is.list(body_obj$tool_choice)) {
    if (
      !is.null(body_obj$tool_choice$`function`$name) &&
        is.null(body_obj$tool_choice$name)
    ) {
      body_obj$tool_choice$name <- body_obj$tool_choice$`function`$name
    }
  }
  httr2::req_body_json(req, body_obj, auto_unbox = TRUE)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.