defmodule ExOpenAi.Api do
@moduledoc """
The main module for the ExOpenAi API client.
Provides a wrapper around Httpoison for making requests to the OpenAI API.
"""
# use HTTPoison.Base
use Tesla
alias Tesla.Multipart
alias ExOpenAi.Config
alias ExOpenAi.Parser
alias ExOpenAi.URLGenerator, as: URL
alias ExOpenAi.StreamProcessor
@type data :: map | list
@doc """
Create a new resource in the OpenAI API with a POST request.
## Examples
ExOpenAi.Api.create(ExOpenAi.Completion, [prompt: "tell me a joke", max_tokens: 500])
{:ok, %Completion{ ... }}
ExOpenAi.Api.create(ExOpenAi.Completion, [])
{:error, %{"model" => "model is required"}, 400}
"""
@spec create(atom, data :: list | map, list) :: Parser.success() | Parser.error()
def create(module, data, options \\ []) do
data = format_data(data)
url = module
|> URL.infer_url()
client()
|> post!(url, data, options)
|> Parser.parse(module, options[:simple])
end
# %{messages: [%{role: "use", content: "what is 2 + 2?"}], model: "gpt-4-turbo"}
@doc """
Creates a stream using the provided module, data, and options.
The stream is created using the `Stream.resource/3` function, where the initial function posts the data with `Api.post!/4`, the intermediate function processes the async response with `StreamProcessor.handle_async_response/2`, and the after function closes the async response with `StreamProcessor.close_async_response/1`.
## Parameters
* `module` - The module to be used for handling the data stream.
* `data` - A keyword list containing the data to be sent in the request.
* `options` - An optional keyword list of additional options for the request.
## Returns
* A stream that is created using the `Stream.resource/3` function, allowing processing of the data as it is received.
## Usage
This function is used to create a stream with the specified module, process ID, data, and options. This enables efficient handling of streaming data from the OpenAI API.
## Note that this is the only function that uses HTTPoison, Tesla does not have good support for streaming.
"""
@spec create_stream(atom, data :: list | map, list) :: any()
def create_stream(module, data, options \\ []) do
data =
cond do
is_list(data) ->
data
|> Keyword.put(:stream, true)
is_map(data) ->
data
|> Map.put(:stream, true)
end
|> format_data()
|> Jason.encode!()
url =
module
|> URL.infer_url()
Stream.resource(
fn ->
HTTPoison.post!(url, data, process_request_headers(options),
stream_to: self(),
async: :once,
recv_timeout: Config.timeout()
)
end,
&StreamProcessor.handle_async_response(&1, module),
&StreamProcessor.close_async_response/1
)
end
@doc """
Create with a file upload.
Thing to note:
- The `file` parameter is required
- Additional data that has a list of binaries in it is currently supported. But if the nature of the form shifts so that nested data within a value may not work.
## Examples
ExOpenAi.Api.create_with_file(ExOpenAi.Completion, [prompt: "tell me a joke", max_tokens: 500])
{:ok, %Completion{ ... }}
ExOpenAi.Api.create_with_file(ExOpenAi.Completion, [])
{:error, %{"model" => "model is required"}, 400}
"""
@spec create_with_file(atom, data :: list | map, atom, list) :: Parser.success() | Parser.error()
def create_with_file(module, data, file_key \\ :atom, options \\ [])
def create_with_file(module, data, file_key, options) when is_list(data) do
# path to file
file = Keyword.get(data, file_key)
mp = Multipart.new()
|> Multipart.add_file(file, name: Atom.to_string(file_key))
additional_data =
data
|> Keyword.delete(file_key)
|> Enum.reduce(mp, fn
{k, v}, acc when is_list(v) ->
Enum.reduce(v, acc, fn v, acc -> Multipart.add_field(acc, "#{k}[]", v) end)
{k, v}, acc ->
acc
|> Multipart.add_field(k, v)
end)
url = module
|> URL.infer_url()
client()
|> post!(url, additional_data, options)
|> Parser.parse(module, options[:simple])
end
def create_with_file(module, data, file_key, options) when is_map(data) do
create_with_file(module, Map.to_list(data), file_key, options)
end
@doc """
List all the resource in the OpenAI API.
## Examples
ExOpenAi.Api.list(ExOpenAi.Completion)
{:ok, [%Completion{ ... }]}
"""
@spec list(atom, list) :: Parser.success() | Parser.error()
def list(module, options \\ []) do
url = module
|> URL.infer_url()
client()
|> get!(url, options)
|> Parser.parse_list(module, options[:simple])
end
@doc """
Retrieve a resource from the OpenAI API.
## Examples
ExOpenAi.Api.retrieve(ExOpenAi.Modle, "gpt-4-turbo")
{:ok, %Completion{ ... }}
"""
@spec retrieve(atom, String.t(), list) :: Parser.success() | Parser.error()
def retrieve(module, id, options \\ []) do
url = module
|> URL.infer_url()
client()
|> get!(url <> "/" <> id, options)
|> Parser.parse(module, options[:simple])
end
@doc """
Delete a resource from the OpenAI API.
## Examples
ExOpenAi.Api.delete(ExOpenAi.Modle, "gpt-4-turbo")
{:ok, %Completion{ ... }}
"""
@spec remove(atom, String.t(), list) :: Parser.success() | Parser.error()
def remove(module, id, options \\ []) do
url = module
|> URL.infer_url()
client()
|> delete!(url <> "/" <> id, options)
|> Parser.parse_delete()
end
@doc """
Builds the authorization header for the request.
"""
@spec auth_header(options :: list) :: list
def auth_header(options \\ []) do
auth_header([], {options[:api_key], options[:organization]})
end
@doc """
Builds the actual authorization header for the request.
## Examples
iex> ExOpenAi.Api.auth_header([], {"api123", "org345"})
[{"Authorization", "Bearer api123"}, {"OpenAI-Organization", "org345"}]
iex> ExOpenAi.Api.auth_header([], {"api123", nil})
[{"Authorization", "Bearer api123"}]
"""
@spec auth_header(header :: list, tuple()) :: list
def auth_header(headers, {api_key, organzation}) do
headers
|> put_header("Authorization", "Bearer #{api_key}")
|> put_header("OpenAI-Organization", organzation)
end
defp put_header(headers, _key, value) when is_nil(value), do: headers
defp put_header(headers, key, value), do: headers ++ [{key, value}]
@spec format_data(list | map) :: binary
def format_data(data)
def format_data(data) when is_list(data) do
data
|> Map.new()
end
def format_data(data) when is_map(data), do: data
def ensure_valid_filename(filename) do
URI.encode(filename)
end
###
# TESLA API
###
def process_request_headers(headers \\ []) do
headers
|> Keyword.put(:"Content-Type", "application/json")
|> auth_header({Config.api_key(), Config.organization()})
end
def client() do
[
{Tesla.Middleware.Timeout, timeout: Config.timeout()},
{Tesla.Middleware.Headers, process_request_headers()},
Tesla.Middleware.EncodeJson
]
|> Tesla.client()
end
end