Packages
fnord
0.9.27
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/store/api_usage.ex
defmodule Store.APIUsage do
@moduledoc """
Module for recording and checking API usage data. Cross OS process
coordination is handled with regard to file reads/writes. It is up to the
caller to ensure that requests are internally ordered for consistency.
"""
@path "usage.json"
@tag "[usage]"
@re_reset ~r/^(\d+(?:\.\d+)?)(ms|s)$/
@debug_env_var "FNORD_DEBUG_API_USAGE"
@type model_usage :: %{
updated_at: non_neg_integer,
requests_max: non_neg_integer,
requests_left: non_neg_integer,
requests_reset: non_neg_integer,
tokens_max: non_neg_integer,
tokens_left: non_neg_integer,
tokens_reset: non_neg_integer
}
@type usage :: %{
optional(binary) => model_usage
}
@type status :: non_neg_integer
@type ok_result ::
{:ok,
%{
body: map,
headers: list,
status: status
}}
@type http_result ::
ok_result
| {:http_error, {status, String.t()}}
| {:transport_error, any}
@doc """
Returns the path to the usage store file.
"""
@spec store_path() :: binary
def store_path do
Path.join(Store.store_home(), @path)
end
@doc """
Records API usage for a provided model. If model is nil, returns http_result
unchanged. For 2xx {:ok, %{headers: headers, status: code}} results and a
binary model, records usage data and updates the store. Other responses or
errors are returned unchanged.
"""
@spec record_for_model(binary | nil, http_result) :: http_result
def record_for_model(nil, http_result), do: http_result
def record_for_model(_, {:http_error, _} = http_result), do: http_result
def record_for_model(_, {:transport_error, _} = http_result), do: http_result
def record_for_model(_, {:ok, %{status: code} = payload})
when code < 200 or
code >= 300 do
{:ok, payload}
end
# HTTP success responses
def record_for_model(model, {:ok, %{headers: headers} = payload}) do
ensure_store_file()
headers_map = Enum.into(headers, %{})
with {:ok, usage} <- collect_usage_data(headers_map),
:ok <- update_file(model, usage) do
case Util.Env.get_env(@debug_env_var, nil) do
"" -> :ok
"0" -> :ok
nil -> :ok
_ -> UI.debug(@tag, "#{model}: #{inspect(usage, pretty: true)}")
end
else
_reason -> :ok
end
{:ok, payload}
end
@doc """
Checks if a request can be made for the given model based on the last
recorded usage data for the model. If a request can be made, returns `:ok`.
Otherwise, returns `{:wait, milliseconds}` indicating how long to wait before
attempting another request.
"""
@spec check(binary) ::
:ok
| {:wait, non_neg_integer}
| {:error, term}
def check(model) do
ensure_store_file()
with :ok <- can_request?(model) do
:ok
end
end
# ----------------------------------------------------------------------------
# Internals
# ----------------------------------------------------------------------------
@spec can_request?(binary) ::
:ok
| {:wait, non_neg_integer}
| {:error, term}
defp can_request?(model) do
FileLock.with_lock(store_path(), fn ->
with {:ok, data} <- read_file() do
usage = Map.get(data, model, %{})
updated_at = Map.get(usage, :updated_at, 0)
elapsed_time = now() - updated_at
requests_reset = Map.get(usage, :requests_reset, 0)
if requests_reset <= elapsed_time do
:ok
else
{:wait, requests_reset - elapsed_time}
end
end
end)
|> case do
{:ok, result} -> result
{:callback_error, exc} -> {:error, exc}
{:error, reason} -> {:error, reason}
end
end
@spec update_file(binary, model_usage) :: :ok | {:error, term}
defp update_file(model, usage) do
FileLock.with_lock(store_path(), fn ->
with {:ok, usage_data} <- read_file() do
usage = Map.put(usage, :updated_at, now())
payload = Map.put(usage_data, model, usage)
write_file(payload)
end
end)
|> case do
{:ok, result} -> result
{:callback_error, exc} -> {:error, exc}
{:error, reason} -> {:error, reason}
end
end
@spec read_file() :: {:ok, usage} | {:error, term}
defp read_file do
with {:ok, json} <- File.read(store_path()),
{:ok, data} <- SafeJson.decode(json) do
data
|> Enum.map(fn {model, usage} ->
{model, Util.string_keys_to_atoms(usage)}
end)
|> Enum.into(%{})
|> then(&{:ok, &1})
end
end
@spec write_file(map) :: :ok | {:error, term}
defp write_file(data) do
with {:ok, json} <- SafeJson.encode(data, pretty: true) do
File.write(store_path(), json)
end
end
@spec collect_usage_data(map) :: {:ok, model_usage} | {:error, atom, binary}
defp collect_usage_data(headers) do
with {:ok, requests_max} <- collect_usage_metric(headers, "x-ratelimit-limit-requests"),
{:ok, requests_left} <- collect_usage_metric(headers, "x-ratelimit-remaining-requests"),
{:ok, requests_reset} <- collect_reset_metric(headers, "x-ratelimit-reset-requests"),
{:ok, tokens_max} <- collect_usage_metric(headers, "x-ratelimit-limit-tokens"),
{:ok, tokens_left} <- collect_usage_metric(headers, "x-ratelimit-remaining-tokens"),
{:ok, tokens_reset} <- collect_reset_metric(headers, "x-ratelimit-reset-tokens") do
{:ok,
%{
updated_at: now(),
requests_max: requests_max,
requests_left: requests_left,
requests_reset: requests_reset,
tokens_max: tokens_max,
tokens_left: tokens_left,
tokens_reset: tokens_reset
}}
end
end
@spec collect_usage_metric(map, binary) :: {:ok, non_neg_integer} | {:error, atom, binary}
defp collect_usage_metric(headers, key) do
headers
|> Map.fetch(key)
|> case do
{:ok, value} ->
value
|> Integer.parse()
|> case do
{int_value, _} -> {:ok, int_value}
:error -> {:error, :invalid_integer}
end
:error ->
{:error, :not_found, key}
end
end
@spec collect_reset_metric(map, binary) :: {:ok, non_neg_integer} | {:error, atom, binary}
defp collect_reset_metric(headers, key) do
headers
|> Map.fetch(key)
|> case do
# In "2ms" or "1s" format
{:ok, value} -> parse_reset_metric(value)
:error -> {:error, :not_found, key}
end
end
@spec parse_reset_metric(binary) :: {:ok, non_neg_integer} | {:error, atom, binary}
defp parse_reset_metric(value) do
with [amount, unit] <- Regex.run(@re_reset, value, capture: :all_but_first),
{float_value, _rest} <- Float.parse(amount) do
ms_value =
case unit do
"ms" -> float_value
"s" -> float_value * 1000.0
end
# Be conservative: never under-wait.
|> Float.ceil()
|> trunc()
|> max(0)
{:ok, ms_value}
else
_ -> {:error, :invalid_reset_format, value}
end
end
@spec now() :: non_neg_integer
defp now do
DateTime.utc_now()
|> DateTime.to_unix(:millisecond)
end
@spec ensure_store_file() :: :ok | {:error, term}
defp ensure_store_file() do
path = store_path()
unless File.exists?(path) do
File.write(path, "{}")
end
:ok
end
end