R/provider_anthropic.R

Defines functions create_anthropic stream_anthropic_failover stream_anthropic is_empty_args

Documented in create_anthropic stream_anthropic

#' @name provider_anthropic
#' @title Anthropic Provider
#' @description
#' Implementation for Anthropic Claude models.
#' @keywords internal
NULL

#' @title Anthropic Language Model Class
#' @description
#' Language model implementation for Anthropic's Messages API.
#' @keywords internal
AnthropicLanguageModel <- R6::R6Class(
  "AnthropicLanguageModel",
  inherit = LanguageModelV1,
  private = list(
    config = NULL,
    get_headers = function() {
      h <- list(
        `Content-Type` = "application/json",
        `anthropic-version` = private$config$api_version
      )
      if (nzchar(private$config$api_key %||% "")) {
        h$`x-api-key` <- private$config$api_key
      }

      # Merge custom headers first so we can check if they override defaults
      if (!is.null(private$config$headers)) {
        for (name in names(private$config$headers)) {
          h[[name]] <- private$config$headers[[name]]
        }
      }

      # Add beta header for prompt caching if enabled, unless already specified
      if (!is.null(private$config$enable_caching) && private$config$enable_caching) {
        if (is.null(h$`anthropic-beta`)) {
          h$`anthropic-beta` <- "prompt-caching-2024-07-31"
        }
      }

      h
    },

    # Convert standard messages to Anthropic format
    # Anthropic requires system messages to be passed as a separate `system` parameter
    format_messages = function(messages) {
      system_prompt <- NULL
      system_cache_control <- NULL
      formatted <- list()

      for (msg in messages) {
        if (msg$role == "system") {
          # Anthropic: system message is a top-level parameter
          system_prompt <- if (is.character(msg$content) || is.null(msg$content)) {
            msg$content
          } else {
            content_blocks_to_text(msg$content, arg_name = "system")
          }
          # Check for cache_control in system message
          if (!is.null(msg$cache_control)) {
            system_cache_control <- msg$cache_control
          }
        } else {
          # user and assistant roles
          content <- if (is.character(msg$content) || is.null(msg$content)) {
            msg$content
          } else {
            translate_message_content(msg$content, target = "anthropic")
          }

          # Construct message object
          msg_obj <- list(
            role = msg$role,
            content = content
          )

          # Pass through cache_control if present (for Prompt Caching)
          if (!is.null(msg$cache_control)) {
            # For caching, content might need to be a list of blocks if not already
            if (is.character(content)) {
              msg_obj$content <- list(
                list(
                  type = "text",
                  text = content,
                  cache_control = msg$cache_control
                )
              )
            } else if (is.list(content)) {
              # Assume user already structured it or we need to attach to last block
              # For simplicity, we assume if cache_control is on the message,
              # it's intended for the content block.
              # Users should ideally structure content blocks themselves for advanced caching.
              # But if they passed cache_control on the message level:
              msg_obj$content <- lapply(content, function(block) {
                block$cache_control <- msg$cache_control
                block
              })
            }
          }

          formatted <- c(formatted, list(msg_obj))
        }
      }

      # Format system prompt with cache_control if needed
      if (!is.null(system_prompt)) {
        if (!is.null(system_cache_control)) {
          system_prompt <- list(
            list(
              type = "text",
              text = system_prompt,
              cache_control = system_cache_control
            )
          )
        }
      }

      list(
        system = system_prompt,
        messages = formatted
      )
    }
  ),
  public = list(
    #' @description Initialize the Anthropic language model.
    #' @param model_id The model ID (e.g., "claude-sonnet-4-20250514").
    #' @param config Configuration list with api_key, base_url, headers, etc.
    #' @param capabilities Optional list of capability flags.
    initialize = function(model_id, config, capabilities = list()) {
      super$initialize(
        provider = config$provider_name %||% "anthropic",
        model_id = model_id,
        capabilities = capabilities
      )
      private$config <- config
    },

    #' @description Get the configuration list.
    #' @return A list with provider configuration.
    get_config = function() {
      private$config
    },

    #' @description Generate text (non-streaming).
    #' @param params A list of call options including messages, temperature, etc.
    #' @return A GenerateResult object.
    do_generate = function(params) {
      url <- api_endpoint_urls(private$config, "/messages")
      headers <- private$get_headers()

      # Format messages for Anthropic API
      formatted <- private$format_messages(params$messages)

      body <- list(
        model = self$model_id,
        max_tokens = params$max_tokens %||% 4096 # Anthropic requires max_tokens
      )

      # Add system prompt if present
      if (!is.null(formatted$system)) {
        body$system <- formatted$system
      }

      body$messages <- formatted$messages

      # Force temperature = 1.0 for thinking models (Anthropic requirement)
      # Thinking can be enabled via explicit parameter or detected by model name (for proxies)
      is_thinking <- !is.null(params$thinking) || grepl("-think", self$model_id, fixed = TRUE)

      if (is_thinking) {
        if (!is.null(params$temperature) && params$temperature != 1.0) {
          if (interactive()) {
            cli::cli_alert_info("Anthropic thinking models require temperature = 1.0. Overriding {params$temperature}.")
          }
        }
        body$temperature <- 1.0
      } else if (!is.null(params$temperature)) {
        body$temperature <- params$temperature
      }

      if (!is.null(params$top_p)) {
        body$top_p <- params$top_p
      }
      if (!is.null(params$stop_sequences)) {
        body$stop_sequences <- params$stop_sequences
      }

      # Add tools if provided (Anthropic format)
      if (!is.null(params$tools) && length(params$tools) > 0) {
        body$tools <- lapply(params$tools, function(t) {
          if (inherits(t, "Tool")) {
            tool_api_fmt <- t$to_api_format("anthropic")
            if (!is.null(t$meta) && !is.null(t$meta$cache_control)) {
              tool_api_fmt$cache_control <- t$meta$cache_control
            }
            tool_api_fmt
          } else {
            t # Assume already in correct format
          }
        })
      }

      # NEW: Pass through any extra parameters
      handled_params <- c(
        "messages", "temperature", "max_tokens", "tools", "stream", "model",
        "system", "top_p", "stop_sequences",
        "timeout_seconds", "total_timeout_seconds", "first_byte_timeout_seconds",
        "connect_timeout_seconds", "idle_timeout_seconds"
      )
      extra_params <- params[setdiff(names(params), handled_params)]
      if (length(extra_params) > 0) {
        body <- utils::modifyList(body, extra_params)
      }

      # Remove NULL entries
      body <- body[!sapply(body, is.null)]

      response <- post_to_api(
        url,
        headers,
        body,
        timeout_seconds = params$timeout_seconds %||% private$config$timeout_seconds,
        total_timeout_seconds = params$total_timeout_seconds %||% private$config$total_timeout_seconds,
        first_byte_timeout_seconds = params$first_byte_timeout_seconds %||% private$config$first_byte_timeout_seconds,
        connect_timeout_seconds = params$connect_timeout_seconds %||% private$config$connect_timeout_seconds,
        idle_timeout_seconds = params$idle_timeout_seconds %||% private$config$idle_timeout_seconds
      )

      # Parse Anthropic response format
      # Response has: id, type, role, content (array), model, stop_reason, usage
      text <- ""
      tool_calls <- NULL

      if (length(response$content) > 0) {
        for (block in response$content) {
          if (block$type == "text") {
            text <- paste0(text, block$text)
          } else if (block$type == "tool_use") {
            # Anthropic tool_use block
            if (is.null(tool_calls)) tool_calls <- list()
            tool_calls <- c(tool_calls, list(list(
              id = block$id,
              name = block$name,
              arguments = block$input
            )))
          }
        }
      }

      GenerateResult$new(
        text = text,
        usage = list(
          prompt_tokens = response$usage$input_tokens,
          completion_tokens = response$usage$output_tokens,
          total_tokens = response$usage$input_tokens + response$usage$output_tokens
        ),
        finish_reason = response$stop_reason,
        raw_response = response,
        tool_calls = tool_calls
      )
    },

    #' @description Generate text (streaming).
    #' @param params A list of call options.
    #' @param callback A function called for each chunk: callback(text, done).
    #' @return A GenerateResult object.
    do_stream = function(params, callback) {
      url <- api_endpoint_urls(private$config, "/messages")
      headers <- private$get_headers()

      # Format messages for Anthropic API
      formatted <- private$format_messages(params$messages)

      body <- list(
        model = self$model_id,
        max_tokens = params$max_tokens %||% 4096,
        stream = TRUE
      )

      # Add system prompt if present
      if (!is.null(formatted$system)) {
        body$system <- formatted$system
      }

      body$messages <- formatted$messages

      # Optional parameters
      is_thinking <- !is.null(params$thinking) || grepl("-think", self$model_id, fixed = TRUE)
      if (is_thinking) {
        body$temperature <- 1.0
      } else if (!is.null(params$temperature)) {
        body$temperature <- params$temperature
      }

      # Add tools if provided (Anthropic format)
      if (!is.null(params$tools) && length(params$tools) > 0) {
        body$tools <- lapply(params$tools, function(t) {
          if (inherits(t, "Tool")) {
            tool_api_fmt <- t$to_api_format("anthropic")
            if (!is.null(t$meta) && !is.null(t$meta$cache_control)) {
              tool_api_fmt$cache_control <- t$meta$cache_control
            }
            tool_api_fmt
          } else {
            t # Assume already in correct format
          }
        })
      }

      body <- body[!sapply(body, is.null)]

      # Anthropic uses different SSE event format
      stream_anthropic(
        url,
        headers,
        body,
        callback,
        timeout_seconds = params$timeout_seconds %||% private$config$timeout_seconds,
        total_timeout_seconds = params$total_timeout_seconds %||% private$config$total_timeout_seconds,
        first_byte_timeout_seconds = params$first_byte_timeout_seconds %||% private$config$first_byte_timeout_seconds,
        connect_timeout_seconds = params$connect_timeout_seconds %||% private$config$connect_timeout_seconds,
        idle_timeout_seconds = params$idle_timeout_seconds %||% private$config$idle_timeout_seconds
      )
    },

    #' @description Format a tool execution result for Anthropic's API.
    #' @param tool_call_id The ID of the tool call (tool_use_id in Anthropic terms).
    #' @param tool_name The name of the tool (not used by Anthropic but kept for interface consistency).
    #' @param result_content The result content from executing the tool.
    #' @return A list formatted as a message for Anthropic's API.
    format_tool_result = function(tool_call_id, tool_name, result_content) {
      list(
        role = "user",
        content = list(
          list(
            type = "tool_result",
            tool_use_id = tool_call_id,
            content = if (is.character(result_content)) result_content else safe_to_json(result_content, auto_unbox = TRUE)
          )
        )
      )
    },

    #' @description Get the message format for Anthropic.
    get_history_format = function() {
      "anthropic"
    }
  )
)

# Helper function to check if arguments are "empty" (NULL or empty list)
is_empty_args <- function(args) {
  if (is.null(args)) {
    return(TRUE)
  }
  if (is.list(args) && length(args) == 0) {
    return(TRUE)
  }
  return(FALSE)
}


#' @title Stream from Anthropic API
#' @description
#' Makes a streaming POST request to Anthropic and processes their SSE format.
#' Anthropic uses event types like `content_block_delta` instead of OpenAI's format.
#' Also handles OpenAI-compatible format for proxy servers.
#'
#' @param url The API endpoint URL.
#' @param headers A named list of HTTP headers.
#' @param body The request body (will be converted to JSON).
#' @param callback A function called for each text delta.
#' @param max_retries Maximum number of connection/first-event retries
#'   before any stream chunk has been delivered.
#' @param initial_delay_ms Initial delay in milliseconds.
#' @param backoff_factor Multiplier for delay on each retry.
#' @param timeout_seconds Legacy alias for `total_timeout_seconds`.
#' @param total_timeout_seconds Optional total stream timeout in seconds.
#' @param first_byte_timeout_seconds Optional time-to-first-byte timeout in seconds.
#' @param connect_timeout_seconds Optional connection-establishment timeout in seconds.
#' @param idle_timeout_seconds Optional stall timeout in seconds.
#' @return A GenerateResult object.
#' @keywords internal
stream_anthropic <- function(url, headers, body, callback,
                             max_retries = 5,
                             initial_delay_ms = 2000,
                             backoff_factor = 2,
                             timeout_seconds = NULL,
                             total_timeout_seconds = NULL,
                             first_byte_timeout_seconds = NULL,
                             connect_timeout_seconds = NULL,
                             idle_timeout_seconds = NULL) {
  urls <- normalize_api_url_candidates(url)
  if (length(urls) == 0) {
    rlang::abort("`url` must contain at least one non-empty API endpoint URL.")
  }
  if (length(urls) > 1) {
    return(stream_anthropic_failover(
      urls = urls,
      headers = headers,
      body = body,
      callback = callback,
      max_retries = max_retries,
      initial_delay_ms = initial_delay_ms,
      backoff_factor = backoff_factor,
      timeout_seconds = timeout_seconds,
      total_timeout_seconds = total_timeout_seconds,
      first_byte_timeout_seconds = first_byte_timeout_seconds,
      connect_timeout_seconds = connect_timeout_seconds,
      idle_timeout_seconds = idle_timeout_seconds
    ))
  }
  url <- urls[[1]]

  if (!preflight_internet(url)) {
    return(GenerateResult$new())
  }

  timeout_config <- resolve_request_timeout_config(
    timeout_seconds = timeout_seconds,
    total_timeout_seconds = total_timeout_seconds,
    first_byte_timeout_seconds = first_byte_timeout_seconds,
    connect_timeout_seconds = connect_timeout_seconds,
    idle_timeout_seconds = idle_timeout_seconds,
    request_type = "stream"
  )
  req <- httr2::request(url)
  req <- httr2::req_headers(req, !!!headers)
  req <- prepare_json_post_request(req, body)
  req <- apply_request_timeout_config(req, timeout_config)
  req <- httr2::req_error(req, is_error = function(resp) FALSE) # Handle errors manually

  attempt <- 0L
  delay_ms <- initial_delay_ms

  run_stream_attempt <- function() {
    resp <- NULL
    stream_state <- new.env(parent = emptyenv())
    stream_state$delivered_events <- FALSE

    on.exit(stream_response_close(resp), add = TRUE)

    resp <- stream_perform_connection(req)

    status <- stream_response_status(resp)
    if (status >= 400) {
      error_text <- tryCatch(
        stream_response_body_string(resp),
        error = function(e) "Unknown error (could not read body)"
      )
      abort_http_api_error(status = status, url = url, error_body = error_text)
    }

    abort_stream_transport_error <- function(e) {
      stream_class <- if (isTRUE(stream_state$delivered_events)) {
        "aisdk_stream_partial_error"
      } else {
        "aisdk_stream_start_error"
      }
      header <- if (isTRUE(stream_state$delivered_events)) {
        "API stream interrupted after data was received"
      } else {
        "API stream failed before the first event"
      }
      rlang::abort(
        c(
          header,
          "i" = paste0("URL: ", url),
          "x" = conditionMessage(e)
        ),
        class = c(stream_class, request_error_classes(e)),
        parent = e
      )
    }

    agg <- SSEAggregator$new(function(text, done) {
      stream_state$delivered_events <- TRUE
      callback(text, done)
    })

    openai_seen <- FALSE

    debug_opt <- getOption("aisdk.debug", FALSE)
    debug_enabled <- isTRUE(debug_opt) || (is.character(debug_opt) && tolower(debug_opt) %in% c("1", "true", "yes", "on"))
    debug_env <- Sys.getenv("AISDK_DEBUG", "")
    if (nzchar(debug_env) && tolower(debug_env) %in% c("1", "true", "yes", "on")) {
      debug_enabled <- TRUE
    }

    repeat {
      stream_complete <- tryCatch(
        stream_response_is_complete(resp),
        error = function(e) {
          if (is_stream_transport_error(e)) {
            abort_stream_transport_error(e)
          }
          rlang::cnd_signal(e)
        }
      )
      if (isTRUE(stream_complete)) {
        break
      }

      event <- tryCatch(
        stream_response_sse(resp),
        error = function(e) {
          if (is_stream_transport_error(e)) {
            abort_stream_transport_error(e)
          }
          rlang::cnd_signal(e)
        }
      )

      if (is.null(event)) {
        next
      }

      event_type <- event$type
      data_str <- event$data

      event_data <- NULL
      if (!is.null(data_str) && nzchar(data_str)) {
        if (data_str == "[DONE]") {
          break
        }
        tryCatch(
          {
            event_data <- jsonlite::fromJSON(data_str, simplifyVector = FALSE)
          },
          error = function(e) {
            # Skip malformed JSON.
          }
        )
      }

      if (isTRUE(debug_enabled)) {
        debug_evt <- list(
          event_type = event_type,
          data_names = names(event_data),
          has_choices = !is.null(event_data$choices)
        )
        message("aisdk debug: sse_event=", jsonlite::toJSON(debug_evt, auto_unbox = TRUE))
      }

      if (!is.null(event_data)) {
        if (!is.null(event_data$choices)) {
          openai_seen <- TRUE
          map_openai_chunk(event_data, done = FALSE, agg)
        } else {
          should_break <- map_anthropic_chunk(event_type, event_data, agg)
          if (isTRUE(should_break)) {
            break
          }
        }
      }
    }

    if (openai_seen) {
      agg$on_done()
    }

    agg$build_result()
  }

  repeat {
    attempt <- attempt + 1L

    attempt_result <- tryCatch(
      run_stream_attempt(),
      error = function(e) e
    )

    if (!inherits(attempt_result, "error")) {
      return(attempt_result)
    }

    retryable_start_failure <- inherits(attempt_result, "aisdk_stream_start_error") ||
      (!inherits(attempt_result, "aisdk_api_error") && is_stream_transport_error(attempt_result))

    if (retryable_start_failure && attempt <= max_retries) {
      message(sprintf("Network error, retrying stream in %d ms...", delay_ms))
      Sys.sleep(delay_ms / 1000)
      delay_ms <- delay_ms * backoff_factor
      next
    }

    if (retryable_start_failure) {
      abort_retry_api_error(url = url, error = attempt_result)
    }

    rlang::cnd_signal(attempt_result)
  }
}

#' @keywords internal
stream_anthropic_failover <- function(urls, headers, body, callback,
                                      max_retries = 5,
                                      initial_delay_ms = 2000,
                                      backoff_factor = 2,
                                      timeout_seconds = NULL,
                                      total_timeout_seconds = NULL,
                                      first_byte_timeout_seconds = NULL,
                                      connect_timeout_seconds = NULL,
                                      idle_timeout_seconds = NULL) {
  urls <- order_api_url_candidates(urls)
  last_error <- NULL

  for (candidate in urls) {
    result <- tryCatch(
      stream_anthropic(
        url = candidate,
        headers = headers,
        body = body,
        callback = callback,
        max_retries = max_retries,
        initial_delay_ms = initial_delay_ms,
        backoff_factor = backoff_factor,
        timeout_seconds = timeout_seconds,
        total_timeout_seconds = total_timeout_seconds,
        first_byte_timeout_seconds = first_byte_timeout_seconds,
        connect_timeout_seconds = connect_timeout_seconds,
        idle_timeout_seconds = idle_timeout_seconds
      ),
      error = function(e) e
    )

    if (!inherits(result, "error")) {
      mark_api_route_success(candidate)
      return(result)
    }

    last_error <- result
    if (inherits(result, "aisdk_stream_partial_error")) {
      rlang::cnd_signal(result)
    }

    retryable <- inherits(result, "aisdk_stream_start_error") ||
      is_retryable_api_error(result)

    if (!retryable) {
      rlang::cnd_signal(result)
    }

    mark_api_route_failure(candidate, conditionMessage(result))
    if (length(urls) > 1) {
      message("aisdk: API stream route failed before data was received; trying next configured endpoint.")
    }
  }

  if (!is.null(last_error)) {
    abort_retry_api_error(url = paste(urls, collapse = ", "), error = last_error)
  }
  rlang::abort("API stream failed: no API endpoints were attempted.", class = "aisdk_api_error")
}

#' @title Anthropic Provider Class
#' @description
#' Provider class for Anthropic. Can create language models.
#' @export
AnthropicProvider <- R6::R6Class(
  "AnthropicProvider",
  public = list(
    #' @field specification_version Provider spec version.
    specification_version = "v1",

    #' @description Initialize the Anthropic provider.
    #' @param api_key Anthropic API key. Defaults to ANTHROPIC_API_KEY env var.
    #' @param base_url Base URL for API calls. Defaults to https://api.anthropic.com/v1.
    #' @param api_version Anthropic API version header. Defaults to "2023-06-01".
    #' @param headers Optional additional headers.
    #' @param name Optional provider name override.
    #' @param timeout_seconds Legacy alias for `total_timeout_seconds`.
    #' @param total_timeout_seconds Optional total request timeout in seconds for API calls.
    #' @param first_byte_timeout_seconds Optional time-to-first-byte timeout in seconds for API calls.
    #' @param connect_timeout_seconds Optional connection-establishment timeout in seconds for API calls.
    #' @param idle_timeout_seconds Optional stall timeout in seconds for API calls.
    initialize = function(api_key = NULL,
                          base_url = NULL,
                          api_version = NULL,
                          headers = NULL,
                          name = NULL,
                          timeout_seconds = NULL,
                          total_timeout_seconds = NULL,
                          first_byte_timeout_seconds = NULL,
                          connect_timeout_seconds = NULL,
                          idle_timeout_seconds = NULL) {
      raw_base_url <- base_url %||% paste(
        c(
          Sys.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com/v1"),
          Sys.getenv("ANTHROPIC_BASE_URLS", unset = "")
        ),
        collapse = ","
      )
      base_urls <- normalize_base_urls(raw_base_url)
      private$config <- list(
        api_key = api_key %||% Sys.getenv("ANTHROPIC_API_KEY"),
        base_url = base_urls[[1]],
        base_urls = base_urls,
        api_version = api_version %||% "2023-06-01",
        headers = headers,
        enable_caching = FALSE, # Default false
        provider_name = name %||% "anthropic",
        timeout_seconds = timeout_seconds,
        total_timeout_seconds = total_timeout_seconds,
        first_byte_timeout_seconds = first_byte_timeout_seconds,
        connect_timeout_seconds = connect_timeout_seconds,
        idle_timeout_seconds = idle_timeout_seconds
      )

      if (nchar(private$config$api_key) == 0) {
        rlang::warn("Anthropic API key not set. Set ANTHROPIC_API_KEY env var or pass api_key parameter.")
      }
    },

    #' @description Enable or disable prompt caching.
    #' @param enable Logical.
    enable_caching = function(enable = TRUE) {
      private$config$enable_caching <- enable
    },

    #' @description Create a language model.
    #' @param model_id The model ID (e.g., "claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022").
    #' @return An AnthropicLanguageModel object.
    language_model = function(model_id = "claude-sonnet-4-20250514") {
      AnthropicLanguageModel$new(model_id, private$config)
    }
  ),
  private = list(
    config = NULL
  )
)

#' @title Create Anthropic Provider
#' @description
#' Factory function to create an Anthropic provider.
#'
#' @eval generate_model_docs("anthropic")
#'
#' @param api_key Anthropic API key. Defaults to ANTHROPIC_API_KEY env var.
#' @param base_url Base URL for API calls. Defaults to https://api.anthropic.com/v1.
#' @param api_version Anthropic API version header. Defaults to "2023-06-01".
#' @param headers Optional additional headers.
#' @param name Optional provider name override.
#' @param timeout_seconds Legacy alias for `total_timeout_seconds`.
#' @param total_timeout_seconds Optional total request timeout in seconds for API calls.
#' @param first_byte_timeout_seconds Optional time-to-first-byte timeout in seconds for API calls.
#' @param connect_timeout_seconds Optional connection-establishment timeout in seconds for API calls.
#' @param idle_timeout_seconds Optional stall timeout in seconds for API calls.
#' @return An AnthropicProvider object.
#' @export
#' @examples
#' \donttest{
#' if (interactive()) {
#'   anthropic <- create_anthropic(api_key = "sk-ant-...")
#'   model <- anthropic$language_model("claude-sonnet-4-20250514")
#' }
#' }
create_anthropic <- function(api_key = NULL,
                             base_url = NULL,
                             api_version = NULL,
                             headers = NULL,
                             name = NULL,
                             timeout_seconds = NULL,
                             total_timeout_seconds = NULL,
                             first_byte_timeout_seconds = NULL,
                             connect_timeout_seconds = NULL,
                             idle_timeout_seconds = NULL) {
  AnthropicProvider$new(
    api_key = api_key,
    base_url = base_url,
    api_version = api_version,
    headers = headers,
    name = name,
    timeout_seconds = timeout_seconds,
    total_timeout_seconds = total_timeout_seconds,
    first_byte_timeout_seconds = first_byte_timeout_seconds,
    connect_timeout_seconds = connect_timeout_seconds,
    idle_timeout_seconds = idle_timeout_seconds
  )
}

Try the aisdk package in your browser

Any scripts or data that you put into this service are public.

aisdk documentation built on May 29, 2026, 9:07 a.m.