Packages
fnord
0.9.40
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/endpoint/openai.ex
defmodule AI.Endpoint.OpenAI do
@moduledoc """
OpenAI-specific endpoint implementation.
Provides the endpoint path and error classification used by AI.Endpoint's
centralized retry/backoff logic.
"""
@behaviour AI.Endpoint
@base_url "https://api.openai.com"
@impl AI.Endpoint
@spec endpoint_path() :: String.t()
def endpoint_path, do: "#{@base_url}/v1/responses"
@doc """
Provider-specific error classifier for OpenAI/Cloudflare style responses.
See `c:AI.Endpoint.endpoint_error_classify/4` for contract details.
"""
@impl AI.Endpoint
@spec endpoint_error_classify(integer | nil, binary | nil, list | nil, term | nil) ::
:ok | {:retry, atom, non_neg_integer | nil} | {:fail, atom, binary}
def endpoint_error_classify(status, body, _headers, transport_reason) do
# Body is always a binary here when status is set: the HTTP client
# (HTTPoison via Http.post_json) returns binary bodies for every non-2xx
# response. The `is_binary(b)` guards on the 429 clause are paranoia,
# not feature gating - if a non-binary body somehow appeared they'd
# fall through to the catch-all `:ok` (no retry), which is the right
# safe default for "we don't know what this is."
case {status, body, transport_reason} do
{nil, nil, :timeout} ->
{:retry, :network_glitch, nil}
{nil, nil, :closed} ->
{:retry, :network_glitch, nil}
{nil, nil, {:tls_alert, _}} ->
{:retry, :network_glitch, nil}
{nil, nil, {:ssl, _}} ->
{:retry, :network_glitch, nil}
{429, b, _} when is_binary(b) ->
cond do
throttled_json?(b) -> {:retry, :throttled, parse_try_again_ms(b)}
cloudflare_plaintext?(b) -> {:retry, :intermediary, parse_try_again_ms(b) || 300}
true -> {:retry, :throttled, nil}
end
{s, _b, _} when is_integer(s) and s >= 500 and s < 600 ->
{:retry, :server_error, nil}
{401, _b, _} ->
{:fail, :unauthorized, "Unauthorized"}
{403, _b, _} ->
{:fail, :forbidden, "Forbidden"}
_ ->
:ok
end
end
defp throttled_json?(body) do
case SafeJson.decode(body) do
{:ok, %{"error" => %{"code" => code}}} when code in ["rate_limit_exceeded", "rate_limit"] ->
true
_ ->
false
end
end
defp cloudflare_plaintext?(body) when is_binary(body) do
Regex.match?(~r/cloudflare|please\s+try\s+again/i, body)
end
# OpenAI's rate-limit body uses either "try again in 250ms" or "try again
# in 1.566s" depending on the wait. Both may carry a decimal point. The
# retry scheduler is in ms.
defp parse_try_again_ms(body) when is_binary(body) do
cond do
match = Regex.run(~r/try\s+again\s+in\s+(\d+(?:\.\d+)?)\s*ms/i, body) ->
[_, ms_str] = match
max(1, ms_str |> parse_number() |> round())
match = Regex.run(~r/try\s+again\s+in\s+(\d+(?:\.\d+)?)\s*s/i, body) ->
[_, s_str] = match
max(1, (parse_number(s_str) * 1000) |> round())
true ->
nil
end
end
defp parse_number(str) do
case Float.parse(str) do
{n, _} -> n
:error -> 0.0
end
end
end