Packages
fnord
0.9.21
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/embeddings.ex
defmodule AI.Embeddings do
@behaviour AI.Endpoint
@base_url "https://api.openai.com"
# Upper bound on attempts to work around token limit exceeded errors.
@max_attempts 3
# Short wait between retries to avoid server-side rate limiting.
@retry_interval 250
@model "text-embedding-3-large"
@batch_size 300_000
@batch_reduction_factor 0.75
@chunk_size 8192
@impl AI.Endpoint
def endpoint_path(), do: "#{@base_url}/v1/embeddings"
@doc "Returns the embeddings model name."
@spec model_name() :: String.t()
def model_name(), do: @model
@type embedding :: list(float())
@type embeddings :: list(embedding())
@type error ::
{:error, :max_attempts_reached}
| {:error, :http_error}
| {:error, :transport_error}
| {:error, String.t()}
@type attempt :: non_neg_integer()
@type inputs :: list(String.t())
@doc """
Centralizes embeddings generation for all upstream producers and recovers from
oversize inputs by progressively retrying smaller chunks.
"""
@spec get(String.t()) :: {:ok, embeddings} | error
def get(input) do
input
|> split_into_batches()
|> get_batches(1, [])
end
@spec get_batches(inputs, attempt, embeddings) :: {:ok, embeddings} | error
defp get_batches([], _attempt, acc), do: {:ok, acc}
defp get_batches([batch | rest], attempt, acc) do
case process_batch(batch, attempt) do
{:ok, embeddings} ->
get_batches(rest, 1, merge_embeddings(embeddings, acc))
{:retry, smaller_batches} ->
sleep_before_retry(attempt)
get_batches(smaller_batches ++ rest, attempt + 1, acc)
{:error, reason} when is_atom(reason) or is_binary(reason) ->
{:error, reason}
end
end
@spec process_batch(String.t(), attempt) :: {:ok, embeddings} | {:retry, inputs} | error
defp process_batch(batch, attempt) do
batch
|> split_into_chunks(attempt)
|> endpoint()
|> case do
{:ok, embeddings} ->
{:ok, embeddings}
{:error, :token_limit_exceeded} ->
retry_with_smaller_batch(batch, attempt)
{:error, reason} when is_atom(reason) or is_binary(reason) ->
{:error, reason}
end
end
@spec retry_with_smaller_batch(String.t(), attempt) :: {:retry, inputs} | error
defp retry_with_smaller_batch(_batch, attempt) when attempt >= @max_attempts do
{:error, :max_attempts_reached}
end
defp retry_with_smaller_batch(batch, attempt) do
split_batch_for_retry(batch, attempt + 1)
end
@spec split_batch_for_retry(String.t(), attempt) :: {:retry, inputs} | error
defp split_batch_for_retry(batch, attempt) do
reduction_factor = token_reduction_factor(attempt)
smaller_batches = AI.PretendTokenizer.chunk(batch, @chunk_size, reduction_factor)
case smaller_batches do
[] ->
{:error, :max_attempts_reached}
[^batch] ->
{:error, :max_attempts_reached}
batches ->
{:retry, batches}
end
end
@spec sleep_before_retry(attempt) :: :ok
defp sleep_before_retry(attempt) when attempt > 1 do
Process.sleep(@retry_interval)
:ok
end
defp sleep_before_retry(_attempt), do: :ok
@spec split_into_batches(String.t()) :: inputs
defp split_into_batches(input) do
tokens = AI.PretendTokenizer.guesstimate_tokens(input)
if tokens >= @batch_size do
AI.PretendTokenizer.chunk(input, @batch_size, @batch_reduction_factor)
else
[input]
end
end
@spec split_into_chunks(String.t(), attempt) :: inputs
defp split_into_chunks(batch, attempt) do
reduction_factor = token_reduction_factor(attempt)
AI.PretendTokenizer.chunk(batch, @chunk_size, reduction_factor)
end
# -----------------------------------------------------------------------------
# For each dimension, find the maximum value across all embeddings. This
# isn't necessarily the _most_ accurate, but it selects the highest rating
# for each dimension found in the file, which should be reasonable for
# semantic searching.
# -----------------------------------------------------------------------------
@spec merge_embeddings(embeddings, embedding) :: embeddings | error
defp merge_embeddings([], acc), do: acc
defp merge_embeddings([first | rest], []), do: merge_embeddings(rest, first)
defp merge_embeddings([first | rest], acc) do
merged = Enum.zip_with(acc, first, &max/2)
merge_embeddings(rest, merged)
end
@spec get_api_key!() :: String.t()
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
# -----------------------------------------------------------------------------
# Calculate the token reduction factor based on the number of attempts. This
# is used to dial back the number of (estimated) tokens sent to the endpoint
# when retrying requests.
# -----------------------------------------------------------------------------
@spec token_reduction_factor(attempt) :: float()
defp token_reduction_factor(attempt) do
case attempt do
1 -> 0.75
2 -> 0.50
_ -> 0.25
end
end
@spec token_limit_error?(String.t()) :: boolean
defp token_limit_error?(body) do
case decode_body(body) do
{:ok, decoded_body} ->
token_limit_error_in_decoded_body?(decoded_body) or token_limit_phrase?(body)
:error ->
token_limit_phrase?(body)
end
end
@spec decode_body(String.t()) :: {:ok, term()} | :error
defp decode_body(body) do
case SafeJson.decode(body) do
{:ok, decoded_body} -> {:ok, decoded_body}
{:error, _reason} -> :error
end
end
@spec token_limit_error_in_decoded_body?(term()) :: boolean
defp token_limit_error_in_decoded_body?(decoded_body)
defp token_limit_error_in_decoded_body?(%{"error" => error}) do
token_limit_error_map?(error) or token_limit_error_string?(error)
end
defp token_limit_error_in_decoded_body?(%{"message" => message}) do
token_limit_error_string?(message)
end
defp token_limit_error_in_decoded_body?(%{"errors" => errors}) when is_list(errors) do
Enum.any?(errors, &token_limit_error_in_decoded_body?/1)
end
defp token_limit_error_in_decoded_body?(%{} = decoded_body) do
Enum.any?(decoded_body, fn {_key, value} -> token_limit_error_in_decoded_body?(value) end)
end
defp token_limit_error_in_decoded_body?(decoded_body) when is_list(decoded_body) do
Enum.any?(decoded_body, &token_limit_error_in_decoded_body?/1)
end
defp token_limit_error_in_decoded_body?(_), do: false
@spec token_limit_error_map?(map()) :: boolean
defp token_limit_error_map?(error) when is_map(error) do
token_limit_error_code?(Map.get(error, "code")) or
token_limit_error_code?(Map.get(error, :code)) or
token_limit_error_string?(Map.get(error, "message")) or
token_limit_error_string?(Map.get(error, :message)) or
token_limit_error_in_decoded_body?(Map.get(error, "error")) or
token_limit_error_in_decoded_body?(Map.get(error, :error))
end
defp token_limit_error_map?(_), do: false
@spec token_limit_error_string?(term()) :: boolean
defp token_limit_error_string?(value) when is_binary(value) do
token_limit_phrase?(value)
end
defp token_limit_error_string?(_), do: false
@spec token_limit_phrase?(String.t()) :: boolean
defp token_limit_phrase?(body) do
String.contains?(body, [
"maximum input length",
"maximum context length",
"token limit",
"context length"
])
end
@spec token_limit_error_code?(term()) :: boolean
defp token_limit_error_code?(code) when is_binary(code) do
code in ["context_length_exceeded", "token_limit_exceeded", "max_tokens_exceeded"]
end
defp token_limit_error_code?(_), do: false
@spec endpoint(inputs) ::
{:ok, embeddings}
| {:error, :token_limit_exceeded}
| {:error, :http_error}
| {:error, :transport_error}
defp endpoint(input) do
api_key = get_api_key!()
headers = [
{"Authorization", "Bearer #{api_key}"},
{"Content-Type", "application/json"}
]
payload = %{
encoding_format: "float",
model: @model,
input: input
}
AI.Endpoint.post_json(__MODULE__, headers, payload)
|> case do
{:ok, %{body: %{"data" => embeddings}}} ->
{:ok, Enum.map(embeddings, &Map.get(&1, "embedding"))}
{:http_error, {status_code, body}} ->
handle_http_error(status_code, body)
{:transport_error, error} ->
UI.warn("[AI.Embeddings] Error getting embeddings: #{inspect(error)}")
{:error, :transport_error}
end
end
@spec handle_http_error(integer(), String.t()) ::
{:error, :token_limit_exceeded} | {:error, :http_error}
defp handle_http_error(_status_code, body) do
case token_limit_error?(body) do
true -> {:error, :token_limit_exceeded}
false -> warn_http_error(body)
end
end
@spec warn_http_error(String.t()) :: {:error, :http_error}
defp warn_http_error(body) do
UI.warn("[AI.Embeddings] Error getting embeddings: #{body}")
{:error, :http_error}
end
end