lib/openai_ex.ex

defmodule OpenaiEx do
  @moduledoc """
  `OpenaiEx` is an Elixir library that provides a community-maintained client for
  the OpenAI API.

  The library closely follows the structure of the [official OpenAI API client libraries](https://platform.openai.com/docs/api-reference)
  for [Python](https://github.com/openai/openai-python) making it easy to understand
  and reuse existing documentation and code.
  """
  defstruct token: nil,
            organization: nil,
            project: nil,
            beta: nil,
            base_url: "https://api.openai.com/v1",
            receive_timeout: 15_000,
            stream_timeout: :infinity,
            finch_name: OpenaiEx.Finch,
            _ep_path_mapping: &OpenaiEx._identity/1,
            _http_headers: nil

  @doc """
  Creates a new OpenaiEx struct with the specified token and organization.

  See https://platform.openai.com/docs/api-reference/authentication for details.
  """
  def new(token, organization \\ nil, project \\ nil) do
    headers =
      [{"Authorization", "Bearer #{token}"}] ++
        if(is_nil(organization),
          do: [],
          else: [{"OpenAI-Organization", organization}]
        ) ++
        if(is_nil(project),
          do: [],
          else: [{"OpenAI-Project", project}]
        )

    %OpenaiEx{
      token: token,
      organization: organization,
      project: project,
      _http_headers: headers
    }
  end

  @doc """
  Create file parameter struct for use in multipart requests.

  OpenAI API has endpoints which need a file parameter, such as Files and Audio.
  This function creates a file parameter given a name (optional) and content or a local file path.
  """
  def new_file(name: name, content: content) do
    {name, content}
  end

  def new_file(path: path) do
    {path}
  end

  # Globals for internal library use, **not** for public use.

  @assistants_beta_string "assistants=v2"
  @doc false
  def with_assistants_beta(openai = %OpenaiEx{}) do
    openai
    |> Map.put(:beta, @assistants_beta_string)
    |> Map.get_and_update(:_http_headers, fn headers ->
      {headers, headers ++ [{"OpenAI-Beta", @assistants_beta_string}]}
    end)
    |> elem(1)
  end

  # Globals to allow slight changes to API
  # Not public, and with no guarantee that they will continue to be supported.

  @doc false
  def _identity(x), do: x

  @doc false
  def _with_ep_path_mapping(openai = %OpenaiEx{}, ep_path_mapping) do
    openai |> Map.put(:_ep_path_mapping, ep_path_mapping)
  end

  # https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
  @doc false
  def _azure_ep_path_mapping(api_version) do
    fn ep ->
      case ep do
        "/chat/completions" -> "/chat/completions?api-version=#{api_version}"
        "/completions" -> "/completions?api-version=#{api_version}"
        "/embeddings" -> "/embeddings?api-version=#{api_version}"
        _ -> ep
      end
    end
  end

  # Azure OpenAI. Not public and with no guarantee of continued support.
  def _for_azure(openai = %OpenaiEx{}, resource_name, deployment_id, api_version) do
    openai
    |> with_base_url(
      "https://#{resource_name}.openai.azure.com/openai/deployments/#{deployment_id}"
    )
    |> _with_ep_path_mapping(_azure_ep_path_mapping(api_version))
  end

  def _for_azure(azure_api_key, resource_name, deployment_id, api_version) do
    %OpenaiEx{
      _http_headers: [{"api-key", "#{azure_api_key}"}]
    }
    |> _for_azure(resource_name, deployment_id, api_version)
  end

  # Globals for public use.

  def with_base_url(openai = %OpenaiEx{}, base_url) do
    openai |> Map.put(:base_url, base_url)
  end

  def with_additional_headers(openai = %OpenaiEx{}, additional_headers) do
    Map.update(openai, :_http_headers, [], fn existing_headers ->
      existing_headers ++ Enum.to_list(additional_headers)
    end)
  end

  def with_receive_timeout(openai = %OpenaiEx{}, timeout)
      when is_integer(timeout) and timeout > 0 do
    openai |> Map.put(:receive_timeout, timeout)
  end

  def with_stream_timeout(openai = %OpenaiEx{}, timeout)
      when is_integer(timeout) and timeout > 0 do
    openai |> Map.put(:stream_timeout, timeout)
  end

  def with_finch_name(openai = %OpenaiEx{}, finch_name) do
    openai |> Map.put(:finch_name, finch_name)
  end

  @doc false
  def list_query_fields() do
    [
      :after,
      :before,
      :limit,
      :order
    ]
  end
end