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)
  and [JavaScript](https://github.com/openai/openai-node),
  making it easy to understand and reuse existing documentation and code.
  """
  @enforce_keys [:token]
  defstruct token: nil, organization: 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) do
    %OpenaiEx{
      token: token,
      organization: organization
    }
  end

  @doc false
  def middleware(openai = %OpenaiEx{}) do
    mw = [{Tesla.Middleware.BaseUrl, "https://api.openai.com/v1"}]

    headers = [{"Authorization", "Bearer #{openai.token}"}]

    if is_nil(openai.organization) do
      mw ++ [{Tesla.Middleware.Headers, headers}]
    else
      mw ++
        [{Tesla.Middleware.Headers, headers ++ [{"OpenAI-Organization", openai.organization}]}]
    end
  end

  @doc false
  def post(openai = %OpenaiEx{}, url, json: json) do
    (middleware(openai) ++ [Tesla.Middleware.JSON])
    |> Tesla.client()
    |> Tesla.post!(url, json)
    |> Map.get(:body)
  end

  def post(openai = %OpenaiEx{}, url, multipart: multipart) do
    (middleware(openai) ++ [Tesla.Middleware.DecodeJson])
    |> Tesla.client()
    |> Tesla.post!(url, multipart)
    |> Map.get(:body)
  end

  @doc false
  def get(openai = %OpenaiEx{}, url) do
    (middleware(openai) ++ [Tesla.Middleware.JSON])
    |> Tesla.client()
    |> Tesla.get!(url)
    |> Map.get(:body)
  end

  @doc false
  def to_multi_part_form_data(req, file_keys) do
    mp =
      req
      |> Map.drop(file_keys)
      |> Enum.reduce(Tesla.Multipart.new(), fn {k, v}, acc ->
        acc |> Tesla.Multipart.add_field(to_string(k), v)
      end)

    req
    |> Map.take(file_keys)
    |> Enum.reduce(mp, fn {k, v}, acc ->
      {filename, content} =
        case v do
          {f, c} -> {f, c}
          c -> {"", c}
        end

      acc |> Tesla.Multipart.add_file_content(content, filename, name: to_string(k))
    end)
  end
end