From 7b5b7e68dd62afb6a93f1cfd1fad610e795696b0 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Sun, 5 Apr 2026 16:01:29 +0100 Subject: [PATCH] Harden OpenAI api layer - Support rate limiting - Add proper error handling (don't rely on exceptions) Closes #147 --- config/config.exs | 4 +++- config/test.exs | 3 ++- lib/open_ai/api.ex | 43 +++++++++++++++++++++++-------------------- lib/open_ai/config.ex | 11 +++++++++-- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9379f77e..1802aaca 100644 --- a/config/config.exs +++ b/config/config.exs @@ -72,7 +72,9 @@ config :music_library, BraveSearch, user_agent: user_agent, api_cooldown: 1000 -config :music_library, OpenAI, api_key: "change me" +config :music_library, OpenAI, + api_key: "change me", + api_cooldown: 250 # Configure esbuild (the version is required) config :esbuild, diff --git a/config/test.exs b/config/test.exs index 4192dd32..84e69255 100644 --- a/config/test.exs +++ b/config/test.exs @@ -96,7 +96,8 @@ config :music_library, OpenAI, req_options: [ plug: {Req.Test, OpenAI.API}, max_retries: 0 - ] + ], + api_cooldown: 0 config :phoenix_test, :endpoint, MusicLibraryWeb.Endpoint diff --git a/lib/open_ai/api.ex b/lib/open_ai/api.ex index bd807bff..fa55e29b 100644 --- a/lib/open_ai/api.ex +++ b/lib/open_ai/api.ex @@ -3,27 +3,29 @@ defmodule OpenAI.API do @spec gpt(OpenAI.Completion.t(), OpenAI.Config.t()) :: {:ok, map()} | {:error, term()} def gpt(completion, config) do - resp = - config - |> new_request() - |> Req.merge( - url: "/v1/chat/completions", - receive_timeout: 10_000, - connect_options: [timeout: 2_500], - json: %{ - model: completion.model, - messages: [Map.take(completion, [:content, :role])], - response_format: %{type: "json_object"}, - temperature: completion.temperature - } - ) - |> Req.post!() + case config + |> new_request() + |> Req.merge( + url: "/v1/chat/completions", + receive_timeout: 10_000, + connect_options: [timeout: 2_500], + json: %{ + model: completion.model, + messages: [Map.take(completion, [:content, :role])], + response_format: %{type: "json_object"}, + temperature: completion.temperature + } + ) + |> Req.post() do + {:ok, %{status: status, body: body}} when status in 200..299 -> + content = get_in(body, ["choices", Access.at(0), "message", "content"]) + JSON.decode(content) - if resp.status in 200..299 do - content = get_in(resp.body, ["choices", Access.at(0), "message", "content"]) - {:ok, JSON.decode!(content)} - else - {:error, resp.body} + {:ok, %{body: body}} -> + {:error, "OpenAI API error: #{inspect(body)}"} + + {:error, exception} -> + {:error, "Connection error: #{Exception.message(exception)}"} end end @@ -92,6 +94,7 @@ defmodule OpenAI.API do auth: {:bearer, config.api_key} ) |> Req.Request.merge_options(config.req_options) + |> Req.RateLimiter.attach(name: :open_ai, cooldown: config.api_cooldown) |> Req.Request.append_request_steps(log_attempt: &log_attempt/1) |> Req.Request.append_response_steps(log_error: &log_error/1) end diff --git a/lib/open_ai/config.ex b/lib/open_ai/config.ex index 95c4d4ee..7dc3f69f 100644 --- a/lib/open_ai/config.ex +++ b/lib/open_ai/config.ex @@ -1,12 +1,14 @@ defmodule OpenAI.Config do @type t :: %__MODULE__{ api_key: String.t(), - req_options: Keyword.t() + req_options: Keyword.t(), + api_cooldown: non_neg_integer() } @enforce_keys [:api_key] defstruct api_key: "", - req_options: [] + req_options: [], + api_cooldown: 500 @schema NimbleOptions.new!( api_key: [ @@ -17,6 +19,11 @@ defmodule OpenAI.Config do type: :keyword_list, required: false, default: [] + ], + api_cooldown: [ + type: :integer, + required: false, + default: 500 ] )