Packages
fnord
0.8.65
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
@endpoint "https://api.openai.com/v1/chat/completions"
@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, :api_unavailable, any}
| {:error, :context_length_exceeded, non_neg_integer}
@spec get(model, msgs, tools, response_format, web_search?) :: response
def get(model, msgs, tools \\ nil, response_format \\ nil, web_search? \\ false) do
tracking_id = Services.ModelPerformanceTracker.begin_tracking(model)
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(
if web_search? do
%{web_search_options: %{}}
else
%{}
end
)
result =
try do
Http.post_json(@endpoint, headers, payload)
|> case do
{:transport_error, error} ->
get_error(error)
{:http_error, error} ->
error
|> get_error()
|> case do
{:error, %{message: msg}} = error ->
UI.error("HTTP error while calling OpenAI API: #{msg}")
IO.inspect(payload.messages |> Enum.slice(-1, 1), label: "Last message sent")
error
error ->
error
end
{:ok, response} ->
get_response(response, tracking_id)
end
rescue
e in Jason.DecodeError ->
{:error,
%{
http_status: 500,
error: """
JSON decode error: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
"""
}}
e in RuntimeError ->
{:error,
%{
http_status: 500,
error: """
Runtime error: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
"""
}}
e ->
{:error,
%{
http_status: 500,
error: """
Unexpected error: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
"""
}}
end
# Handle error cases for tracking
case result do
{:error, _} ->
Services.ModelPerformanceTracker.end_tracking(tracking_id, %{})
_ ->
# Success cases are already handled in get_response functions
:ok
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(&System.get_env(&1, nil))
|> 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
},
tracking_id
) do
response
|> Map.put("usage", usage)
|> get_response(tracking_id)
end
defp get_response(%{"tool_calls" => tool_calls}, tracking_id) do
# Track tool calls with empty usage data
Services.ModelPerformanceTracker.end_tracking(tracking_id, %{})
{:ok, :tool, Enum.map(tool_calls, &get_tool_call/1)}
end
defp get_response(%{"content" => response, "usage" => usage}, tracking_id) do
# Track the full usage data for performance metrics
Services.ModelPerformanceTracker.end_tracking(tracking_id, usage)
# Return total_tokens for backward compatibility
total_tokens = Map.get(usage, "total_tokens", 0)
{:ok, :msg, response, total_tokens}
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
|> Jason.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
defp get_error(other), do: {:error, inspect(other, pretty: true)}
end