Packages

fnord

0.6.7

AI code archaeology

Current section

Files

Jump to
fnord lib ai openai.ex
Raw

lib/ai/openai.ex

defmodule AI.OpenAI do
@moduledoc """
This module provides functions to interact with the OpenAI API, including
getting embeddings and completions.
"""
@api_key_error "Missing OpenAI API key. Please set the OPENAI_API_KEY environment variable."
@embedding_endpoint "https://api.openai.com/v1/embeddings"
@completion_endpoint "https://api.openai.com/v1/chat/completions"
defstruct [
:api_key,
:http_options
]
def new(http_options) do
workers = Application.get_env(:fnord, :workers, Cmd.default_workers())
%__MODULE__{
api_key: get_api_key(),
http_options:
[hackney_options: [pool: :openai, max_connections: workers]]
|> Keyword.merge(http_options)
}
end
def get_embedding(openai, model, input) do
headers = [get_auth_header(openai)]
payload =
%{encoding_format: "float", input: input}
|> Map.merge(get_model_params(model))
Http.post_json(@embedding_endpoint, headers, payload, openai.http_options)
|> case do
{:ok, %{"data" => [%{"embedding" => embedding}]}} -> {:ok, embedding}
{:error, error} -> {:error, error}
end
end
def get_completion(openai, model, msgs, tools \\ nil) do
headers = [get_auth_header(openai)]
payload =
%{messages: msgs}
|> Map.merge(get_model_params(model))
|> Map.merge(
case tools do
nil -> %{}
tools -> %{tools: tools}
end
)
Http.post_json(@completion_endpoint, headers, payload, openai.http_options)
|> case do
{:ok, response} -> get_completion_response(response)
{:error, error} -> get_error_response(error)
end
end
# -----------------------------------------------------------------------------
# Private functions
# -----------------------------------------------------------------------------
defp get_api_key() do
case System.get_env("OPENAI_API_KEY", nil) do
nil -> raise @api_key_error
api_key -> api_key
end
end
defp get_auth_header(openai) do
{"Authorization", "Bearer #{openai.api_key}"}
end
defp get_model_params(model) when is_binary(model) do
%{model: model}
end
defp get_model_params(%AI.Model{reasoning: reasoning} = model) when is_nil(reasoning) do
%{model: model.model}
end
defp get_model_params(%AI.Model{reasoning: reasoning} = model) do
%{model: model.model, reasoning_effort: reasoning}
end
defp get_completion_response(%{"choices" => [%{"message" => response}]}) do
get_completion_response(response)
end
defp get_completion_response(%{"tool_calls" => tool_calls}) do
{:ok, :tool, Enum.map(tool_calls, &get_tool_call/1)}
end
defp get_completion_response(%{"content" => response}) do
{:ok, :msg, response}
end
defp get_tool_call(%{"id" => id, "function" => %{"name" => name, "arguments" => args}}) do
%{id: id, function: %{name: name, arguments: args}}
end
defp get_error_response(:closed), do: {:error, "Connection closed"}
defp get_error_response(:timeout), do: {:error, "Connection timed out"}
defp get_error_response({http_status, json_error_string}) do
json_error_string
|> Jason.decode()
|> case do
{:ok, %{"error" => %{"code" => code, "message" => msg}}} ->
{:error, %{http_status: http_status, code: code, message: msg}}
{:ok, error} ->
{:error, %{http_status: http_status, error: inspect(error, pretty: true)}}
{:error, _} ->
{:error, %{http_status: http_status, error: json_error_string}}
end
end
end