Packages
fnord
0.9.17
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/completion_api.ex
defmodule AI.CompletionAPI do
@behaviour AI.Endpoint
@base_url "https://api.openai.com"
@type model :: AI.Model.t()
@type msgs :: [map()]
@type tools :: nil | [AI.Tools.tool_spec()]
@type response_format :: nil | map
@type web_search? :: boolean
@type usage :: non_neg_integer
@type msg_response :: {:ok, :msg, binary, usage}
@type tool_response :: {:ok, :tool, list(map)}
@type response ::
msg_response
| tool_response
| {:error, map}
| {:error, binary}
| {:error, :api_unavailable, any}
| {:error, :context_length_exceeded, non_neg_integer}
@impl AI.Endpoint
def endpoint_path, do: "#{@base_url}/v1/chat/completions"
@spec get(model, msgs, tools, response_format, web_search?) :: response
def get(model, msgs, tools \\ nil, response_format \\ nil, web_search? \\ false) do
api_key = get_api_key!()
response_format =
if is_nil(response_format) do
%{type: "text"}
else
response_format
end
headers = [
{"Authorization", "Bearer #{api_key}"},
{"Content-Type", "application/json"}
]
payload =
%{
model: model.model,
messages: msgs,
response_format: response_format
}
|> Map.merge(
case tools do
nil -> %{}
tools -> %{tools: tools}
end
)
|> Map.merge(
case model.reasoning do
:low -> %{reasoning_effort: "low"}
:medium -> %{reasoning_effort: "medium"}
:high -> %{reasoning_effort: "high"}
_ -> %{}
end
)
|> Map.merge(
case model.verbosity do
:low -> %{verbosity: "low"}
:medium -> %{verbosity: "medium"}
:high -> %{verbosity: "high"}
_ -> %{}
end
)
|> Map.merge(
if web_search? do
%{web_search_options: %{}}
else
%{}
end
)
result =
try do
AI.Endpoint.post_json(__MODULE__, headers, payload)
|> case do
{:transport_error, error} ->
get_error(error)
{:http_error, error} ->
case get_error(error) do
{:error, :context_length_exceeded, _usage} = err ->
err
{:error, %{message: msg}} ->
UI.error("HTTP error while calling OpenAI API: #{msg}")
{:error, %{message: msg}}
other ->
other
end
{:ok, %{body: response} = _payload} ->
get_response(response)
end
rescue
e in RuntimeError ->
{:error,
%{
http_status: 500,
error: """
Runtime error: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
"""
}}
e in ArgumentError ->
{:error,
%{
http_status: 500,
error: """
Argument error: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
"""
}}
e ->
{:error,
%{
http_status: 500,
error: """
Unexpected error: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
"""
}}
end
result
end
# -----------------------------------------------------------------------------
# Private functions
# -----------------------------------------------------------------------------
@spec get_api_key!() :: binary
defp get_api_key!() do
["FNORD_OPENAI_API_KEY", "OPENAI_API_KEY"]
|> Enum.find_value(fn k -> Util.Env.get_env(k, nil) end)
|> case do
nil ->
raise "Either FNORD_OPENAI_API_KEY or OPENAI_API_KEY environment variable must be set"
api_key ->
api_key
end
end
defp get_response(%{"choices" => [%{"message" => response}], "usage" => usage}) do
response
|> Map.put("usage", usage)
|> get_response()
end
defp get_response(%{"tool_calls" => tool_calls}) do
{:ok, :tool, Enum.map(tool_calls, &get_tool_call/1)}
end
defp get_response(%{"content" => response, "usage" => usage}) do
# Return total_tokens for backward compatibility
total_tokens = Map.get(usage, "total_tokens", 0)
{:ok, :msg, response, total_tokens}
end
# Fallback clause for unexpected response shapes
defp get_response(unexpected) do
{:error,
%{
http_status: 500,
error: "Unexpected response #{inspect(unexpected)}"
}}
end
defp get_tool_call(%{"id" => id, "function" => %{"name" => name, "arguments" => args}}) do
%{id: id, function: %{name: name, arguments: args}}
end
defp get_error(:closed), do: {:error, "Connection closed"}
defp get_error(:timeout), do: {:error, "Connection timed out"}
defp get_error(:invalid_json_response), do: {:error, "Invalid JSON response"}
defp get_error({502, reason}), do: {:error, :api_unavailable, reason}
defp get_error({503, reason}), do: {:error, :api_unavailable, reason}
defp get_error({504, reason}), do: {:error, :api_unavailable, reason}
defp get_error({http_status, json_error_string}) do
json_error_string
|> SafeJson.decode()
|> case do
{:ok, %{"error" => %{"message" => msg, "code" => "context_length_exceeded"}}} ->
~r/Your messages resulted in (\d+) tokens/
|> Regex.run(msg)
|> case do
nil -> {:error, :context_length_exceeded, -1}
[_, used] -> {:error, :context_length_exceeded, String.to_integer(used)}
end
{: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
# Catch-all for non-tuple errors: convert to binary
defp get_error(other) when not is_tuple(other), do: {:error, to_string(other)}
# Catch-all for other error tuples: inspect for debugging
defp get_error(other), do: {:error, inspect(other, pretty: true)}
end