Packages

fnord

0.9.39

AI code archaeology

Current section

Files

Jump to
fnord lib ai endpoint openai.ex
Raw

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